为什么这个 LINQ 连接语句不能正常工作?

10

我有这个 LINQ 查询:

    // types...
    LinkedList<WeightedItem> itemScores = new LinkedList<WeightedItem>();

    var result = from i in _ctx.Items
                 join s in itemScores on i.Id equals s._id
                 orderby s._score descending
                 select new ItemSearchResult(i, s._score);

    // this fails:
    return result.ToList();

这个错误是由以下代码引起的:

无法创建类型为 'System.Collections.Generic.IEnumerable`1' 的常量值。
在此上下文中,只支持基元类型(例如 Int32、String 和 Guid)。

[编辑] 这是 WeightedItem 的代码:

public class WeightedItem
{
    public int _id;
    public decimal? _score;

    public WeightedItem(int id, decimal? score)
    {
        _id = id;
        _score = score;
    }
}

你能看出我做错了什么吗?代码编译完美,_ctx.Items 和 itemScores 都包含了适当的值。


你能发布 WeightedItem 的代码吗? - Lazarus
显然,WeightedItem不是原始类型。 - DOK
Lazarus,完成了。DOK是什么意思? - Mickel
3个回答

23

是的,代码可以正常编译 - 问题在于它无法将其转换为SQL。当您引用“local”值时,实体框架必须在需要创建SQL查询时确定如何处理它们。基本上,它无法处理在内存中的集合和数据库表之间进行连接的操作。

有一种可能行得通的方法是改用Contains。我不知道LinkedList<T>是否适用于此,但我相信List<T>至少在LINQ to SQL中是可以使用的:

List<int> requiredScoreIds = itemScores.Select(x => x._id).ToList();

var tmp = (from i in _ctx.Items
           where requiredScoreIds.Contains(i.Id)
           orderby s._score descending
           select i).AsEnumerable();

// Now do the join in memory to get the score
var result = from i in tmp
             join s in itemScores on i.Id equals s._id
             select new ItemSearchResult(i, s._score);

现在这个查询使用了内存中的join操作,这有点不必要。你可以使用一个字典来替代:

List<int> requiredScoreIds = itemScores.Select(x => x._id).ToList();

var tmp = (from i in _ctx.Items
           where requiredScoreIds.Contains(i.Id)
           orderby s._score descending
           select i).AsEnumerable();

// Create a map from score ID to actual score
Dictionary<int, decimal?> map = itemScores.ToDictionary(x => x._id,
                                                        x => x._score);

var result = tmp.Select(i => new ItemSearchResult(i, map[i.Id]));

有道理,那么 .AsEnumerable() 执行查询并将结果保存在内存中吗?如果不是,代码的哪个部分执行了这个操作? - Mickel
2
@Mickel:AsEnumerable不会立即执行查询,但它返回的是IEnumerable<T>而不是IQueryable<T>,因此查询的其余部分将使用Enumerable.xxx而不是Queryable.xxx。当该查询最终需要执行时,它将在数据库中执行第一部分,在内存中执行第二部分。 - Jon Skeet

3

您不能在内存中的列表和可查询对象之间进行连接。您需要像这样做:

var criteria = itemScores.Select(x => x._id).ToList();
var result_tag = (from i in _ctx.Items
                 where criteria.Contains(i.ID)
                 select i).ToList();
var result = from i in result_tag
             join s in itemScores on i.ID equals s._id
             orderby s._score descending
             select new ItemSearchResult(i, s._score);

5
啊,糟糕 - Jon Skeet 赢过我 :) - Aviad P.
Jon Skeet 是 StackOverflow 的 Chuck Norris。 - Four_0h_Three

1

如果 _ctx.Items 表所代表的表不是很大,而且您不关心将整个表加载到内存中,然后在内存中进行过滤, 那么您可以简单地交换连接语句中项目的顺序,如下面的代码片段所示:

LinkedList<WeightedItem> itemScores = new LinkedList<WeightedItem>();

var result = from s in itemScores
             join i in _ctx.Items on s._id equals i.Id
             orderby s._score descending
             select new ItemSearchResult(i, s._score);

return result.ToList();

在原始语句中,调用了Queryable扩展方法:

IQueryable<TResult> Queryable.Join<TOuter, TInner, TKey, TResult>(
        this IQueryable<TOuter> outer,
        IEnumerable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<TOuter, TInner, TResult>> resultSelector
)

而在交换后的那个中,调用了Enumerable扩展方法:

IEnumerable<TResult> Enumerable.Join<TOuter, TInner, TKey, TResult>(
        this IEnumerable<TOuter> outer,
        IEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        Func<TOuter, TInner, TResult> resultSelector
)

因此,在最后一条语句中,完整的_ctx.Items表被加载到内存中,然后通过Linq to Objects与itemScores列表进行连接(我不知道LinkedList,我尝试了List)。

我添加了这个答案主要是因为有人可能会反向输入连接,而不会意识到数据库中会发生什么。

虽然这种方式可以用于后台应用程序,但我不建议这样连接,除非涉及的表只由少量记录组成,且应用程序没有明显的性能恶化。毕竟,这种解决方案使代码更简洁。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接