在Linq to Sql中比较可空类型

44

我有一个分类实体,其中有一个可空的ParentId字段。当下面的方法执行并且categoryId为null时,结果似乎为空,但是有些类别具有null ParentId值。

这里出了什么问题,我缺少了什么?

public IEnumerable<ICategory> GetSubCategories(long? categoryId)
{
    var subCategories = this.Repository.Categories.Where(c => c.ParentId == categoryId)
        .ToList().Cast<ICategory>();

    return subCategories;
}

顺便提一下,当我将条件更改为(c.ParentId == null)时,结果似乎正常。


1
我找到了一种方法...会更新... - Marc Gravell
7个回答

55

其他方法:

Where object.Equals(c.ParentId, categoryId)
或者
Where (categoryId == null ? c.ParentId == null : c.ParentId == categoryId)

这对我来说完美无缺。那么,在我们的谓词中,我们应该默认使用Equals(x, y)而不是“==”,还是有其他需要注意的地方? - AngieM
这对我不起作用,抛出了“可空列应该有一个值”的异常。 - AgentFire

30

首先要做的是开启日志记录,以查看生成的TSQL语句;例如:

ctx.Log = Console.Out;

LINQ-to-SQL 似乎在处理 null 值方面有点不一致(取决于字面值还是实际值):
using(var ctx = new DataClasses2DataContext())
{
    ctx.Log = Console.Out;
    int? mgr = (int?)null; // redundant int? for comparison...
    // 23 rows:
    var bosses1 = ctx.Employees.Where(x => x.ReportsTo == (int?)null).ToList();
    // 0 rows:
    var bosses2 = ctx.Employees.Where(x => x.ReportsTo == mgr).ToList();
}

所以我能建议的就是使用带有空值的顶部表单!

即:

Expression<Func<Category,bool>> predicate;
if(categoryId == null) {
    predicate = c=>c.ParentId == null;
} else {
    predicate = c=>c.ParentId == categoryId;
}
var subCategories = this.Repository.Categories
           .Where(predicate).ToList().Cast<ICategory>();

更新-我使用一个自定义的“表达式”将其“正确地”运行:
    static void Main()
    {
        ShowEmps(29); // 4 rows
        ShowEmps(null); // 23 rows
    }
    static void ShowEmps(int? manager)
    {
        using (var ctx = new DataClasses2DataContext())
        {
            ctx.Log = Console.Out;
            var emps = ctx.Employees.Where(x => x.ReportsTo, manager).ToList();
            Console.WriteLine(emps.Count);
        }
    }
    static IQueryable<T> Where<T, TValue>(
        this IQueryable<T> source,
        Expression<Func<T, TValue?>> selector,
        TValue? value) where TValue : struct
    {
        var param = Expression.Parameter(typeof (T), "x");
        var member = Expression.Invoke(selector, param);
        var body = Expression.Equal(
                member, Expression.Constant(value, typeof (TValue?)));
        var lambda = Expression.Lambda<Func<T,bool>>(body, param);
        return source.Where(lambda);
    }

3
看起来没有比这更好的解决方法了。谢谢! - Ali Ersöz
2
我遇到了完全相同的问题,做了同样的变通方法,本来还想问是否有更好的解决办法。看起来似乎没有 :(这种行为真的很不直观。 - Andrew Barrett
4
我认为文字与变量之间的不一致性比直觉上更糟糕。感谢确认我的猜想。+1 - Jodrell
2
这个问题真的应该被解决,我刚刚发现了这个问题,现在我怀疑我在之前的项目中编写的一些代码也可能受到影响,有点糟糕... - Guillaume86
我怀疑这是因为这两种类型不一致 - 我认为其中一个是Monad(http://ericlippert.com/2013/02/25/monads-part-two/),而另一个是“null字面量”http://ericlippert.com/2013/07/25/what-is-the-type-of-the-null-literal/ ... 但我承认这个例子让我感到头晕。 - Dan Esparza

5
我猜测这是由于DBMS的一个相当常见的属性 - 仅仅因为两个东西都为空并不意味着它们相等。稍作解释,尝试执行以下两个查询:
SELECT * FROM TABLE WHERE field = NULL

SELECT * FROM TABLE WHERE field IS NULL

"IS NULL"的原因是在DBMS世界中,NULL!= NULL,因为NULL的意义是值未定义。由于NULL表示未定义,您不能说两个空值相等,因为根据定义,您不知道它们是什么。
当您明确检查“field == NULL”时,LINQ可能会将其转换为“field IS NULL”。但是,当您使用变量时,我猜LINQ不会自动进行该转换。
这里是一个MSDN论坛帖子,其中包含有关此问题的更多信息。
看起来很好的“作弊”的方法是将lambda更改为以下内容:".
c => c.ParentId.Equals(categoryId)

1
您可以通过设置 ansi nulls 开关来更改 MSSQL 中 NULL = NULL 的行为。请参阅:http://msdn.microsoft.com/zh-cn/library/aa259229(SQL.80).aspx - Jakub Šturc
4
您需要使用object.Equals(a, b)进行比较,该方法对我有效,而a.Equals(b)则无效。 - Simon_Weaver

4

您需要使用“等于”运算符:

 var subCategories = this.Repository.Categories.Where(c => c.ParentId.Equals(categoryId))
        .ToList().Cast<ICategory>();

对于可空类型,Equals方法的返回值如下:
  • 如果HasValue属性为false,并且另一个参数为null,则返回true。也就是说,根据定义,两个null值相等。
  • 如果HasValue属性为true,并且由Value属性返回的值等于另一个参数,则返回true。
  • 如果当前Nullable结构的HasValue属性为true,并且另一个参数为null,则返回false。
  • 如果当前Nullable结构的HasValue属性为false,并且另一个参数不为null,则返回false。
  • 如果当前Nullable结构的HasValue属性为true,并且由Value属性返回的值不等于另一个参数,则返回false。
更多信息请参见Nullable<.T>.Equals Method

这是一个恰当的答案。 - tggm
3
我在LinqPad中测试了这个代码,但似乎不起作用。如果你传入'null'字面量,生成的SQL会测试Categories.ParentID IS NULL,就像你期望的那样。但是,如果你传入一个变量,它将测试Categories.ParentID = p0,如果p0为null,则无法工作。@ariel提供的object.Equals(Categories.ParentID, value)方法非常有效。 - Jared Phelps
这个不行。如果我将 categoryId 传入 null,那么 SQL 就会包含奇怪的 (0 = 1) 条件,这是永远不成立的。 - AgentFire

2

或者您可以直接使用这个。它也会转换为一个更好的SQL查询。

Where((!categoryId.hasValue && !c.ParentId.HasValue) || c.ParentId == categoryId)

1

那么像这样更简单的东西呢?

public IEnumerable<ICategory> GetSubCategories(long? categoryId)
{
    var subCategories = this.Repository.Categories.Where(c => (!categoryId.HasValue && c.ParentId == null) || c.ParentId == categoryId)
        .ToList().Cast<ICategory>();

    return subCategories;
}

0

Linq to Entities支持Null Coelescing(??),因此只需将null即时转换为默认值。

Where(c => c.ParentId == categoryId ?? 0)

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