【问题标题】:LINQ to SQL bug (or very strange feature) when using IQueryable, foreach, and multiple Where使用 IQueryable、foreach 和多个 Where 时的 LINQ to SQL 错误(或非常奇怪的功能)
【发布时间】:2010-09-22 12:50:09
【问题描述】:

我遇到了一个场景,其中 LINQ to SQL 的行为非常奇怪。我想知道我是否做错了什么。但我认为它很有可能是一个错误。

下面粘贴的代码不是我的真实代码。这是我为这篇文章创建的简化版本,使用的是 Northwind 数据库。

一点背景知识:我有一个方法,它采用ProductIQueryable 和一个“过滤器对象”(我将在稍后描述)。它应该基于“过滤器对象”在IQueryable 上运行一些“Where”扩展方法,然后返回IQueryable

所谓的“过滤对象”就是一个System.Collections.Generic.List这个结构的匿名类型:{ column = fieldEnum, id = int }

fieldEnum 是Products 表中我可能希望用于过滤的不同列的枚举。

与其进一步解释我的代码是如何工作的,不如看看它会更容易。很容易理解。

enum filterType { supplier = 1, category }
public IQueryable<Product> getIQueryableProducts()
{
    NorthwindDataClassesDataContext db = new NorthwindDataClassesDataContext();
    IQueryable<Product> query = db.Products.AsQueryable();

    //this section is just for the example. It creates a Generic List of an Anonymous Type
    //with two objects. In real life I get the same kind of collection, but it isn't hard coded like here
    var filter1 = new { column = filterType.supplier, id = 7 };
    var filter2 = new { column = filterType.category, id = 3 };
    var filterList = (new[] { filter1 }).ToList();
    filterList.Add(filter2);

    foreach(var oFilter in filterList)
    {
        switch (oFilter.column)
        {
            case filterType.supplier:
                query = query.Where(p => p.SupplierID == oFilter.id);
                break;
            case filterType.category:
                query = query.Where(p => p.CategoryID == oFilter.id);
                break;
            default:
                break;
        }
    }
    return query;
}

所以这里有一个例子。假设 List 包含两个这种匿名类型的项目,{ column = fieldEnum.Supplier, id = 7 }{ column = fieldEnum.Category, id = 3}

运行上述代码后,IQueryable 对象的底层 SQL 查询应包含:

WHERE SupplierID = 7 AND CategoryID = 3

但实际上,在代码运行之后,得到执行的 SQL 是

WHERE SupplierID = 3 AND CategoryID = 3

我尝试将query 定义为属性并在设置器上设置断点,以为我可以捕捉到不应该发生的变化。但据说一切都很好。因此,我只是在每个命令之后检查底层 SQL。我意识到第一个 Where 运行良好,query 保持良好(意思是 SupplierID = 7)直到 foreach 循环第二次运行之后。在oFilter 成为第二个匿名类型项目而不是第一个之后,“查询”SQL 更改为Supplier = 3。所以这里必须发生的事情是,LINQ to SQL 不仅记住了Supplier 应该等于 7,还记住了 Supplier 应该等于 oFilter.id。但是oFilterforeach循环的单个项目的名称,它在迭代之后意味着不同的东西。

【问题讨论】:

  • Brian 已经解决了这个错误(它与表达式的评估时间有关),但请注意,初始查询末尾的“.AsQueryable()”是不必要的,因为 db.Products是 Table,它已经是 IQueryable

标签: linq linq-to-sql


【解决方案1】:

我只看了你的问题,但我 90% 确定你应该阅读 On lambdas, capture, and mutability 的第一部分(其中包括指向 5 个类似 SO 问题的链接),一切都会变得清晰。

它的基本要点是您示例中的变量oFilter 已在闭包中由reference 而不是由value 捕获。这意味着一旦循环完成迭代,变量的引用就是最后一个,因此在 lambda 执行时计算的值也是最后一个。

解决方法是在foreach 循环中插入一个新变量,其作用域仅为该迭代而不是整个循环:

foreach(var oFilter in filterList)
{
    var filter = oFilter; // add this
    switch (oFilter.column) // this doesn't have to change, but can for consistency
    {
        case filterType.supplier:
            query = query.Where(p => p.SupplierID == filter.id); // use `filter` here
            break;

现在每个闭包都在一个 不同 filter 变量之上,该变量在每个循环内重新声明,您的代码将按预期运行。

【讨论】:

    【解决方案2】:

    按设计工作。您面临的问题是词法闭包可变变量之间的冲突。

    你可能想做的是

    foreach(var oFilter in filterList)
    {
        var o = oFilter;
        switch (o.column)
        {
            case filterType.supplier:
                query = query.Where(p => p.SupplierID == o.id);
                break;
            case filterType.category:
                query = query.Where(p => p.CategoryID == o.id);
                break;
            default:
                break;
        }
    }
    

    编译到 IL 时,变量 oFilter 被声明一次并使用 multiply。您需要的是在闭包中为每次使用该变量单独声明的变量,这就是 o 现在的用途。

    当您使用它时,请摆脱那个混蛋的匈牙利符号:P。

    【讨论】:

    • "可变变量" - "o" 不是可变变量吗?也许有更好的方式来描述问题。
    • 你讨厌匈牙利记谱法,真好!
    【解决方案3】:

    我认为这是我见过的最清晰的解释:http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx:

    基本上,问题的出现是因为我们指定foreach循环是一个语法糖

    {
        IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator();
        try
        {
            int m; // OUTSIDE THE ACTUAL LOOP
            while(e.MoveNext())
            {
                m = (int)(int)e.Current;
                funcs.Add(()=>m);
            }
        }
        finally
        {
            if (e != null) ((IDisposable)e).Dispose();
        }
    }
    

    如果我们指定扩展是

    try
    {
        while(e.MoveNext())
        {
            int m; // INSIDE
            m = (int)(int)e.Current;
            funcs.Add(()=>m);
        }
    

    那么代码将按预期运行。

    【讨论】:

      【解决方案4】:

      问题在于您没有附加到查询中,而是每次都通过 foreach 语句替换它。

      你想要 PredicateBuilder 之类的东西 - http://www.albahari.com/nutshell/predicatebuilder.aspx

      【讨论】:

        猜你喜欢
        • 2011-03-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-11-09
        • 1970-01-01
        • 1970-01-01
        • 2011-07-31
        • 1970-01-01
        相关资源
        最近更新 更多