尽管 where 子句要求不为空,EF 核心 6 选择空值

EF core 6 selecting null values despite where clause asking for not null

我有一个这样的 Linq2Sql 查询:

Parent.Include(p => p.Children)
  .Where(p => p.Children.Any(c => c.SomeNullableDateTime == null)
    && p.Children
        .Where(c => c.SomeNullableDateTime == null)
        .OrderBy(c => c.SomeInteger)
        .First()
        .SomeOtherNullableDateTime != null
  )
  .Select(p => p.Children
        .Where(c => c.SomeNullableDateTime == null)
        .OrderBy(c => c.SomeInteger)
        .First()
        .SomeOtherNullableDateTime)
  .ToList();

在从 EF 核心 5 移至 EF 核心 6 之前,此方法运行良好。对于 EF 核心 6,结果列表包含一些空值(不应该是这种情况,因为 where 条件要求不为空)。 EF 核心 6 中是否有一些我不知道的重大更改/限制,或者这只是一个错误?

更新:这是输出的摘录

更新 2:这是生成的 SQL 声明

SELECT(
    SELECT TOP(1)[p1].[SomeOtherNullableDateTime]
    FROM[Children] AS[p1]
    WHERE([p].[Id] = [p1].[ParentId]) AND[p1].[SomeNullableDateTime] IS NULL
    ORDER BY[p1].[SomeInteger])
FROM[Parent] AS[p]
WHERE EXISTS(
    SELECT 1
    FROM[Children] AS [c]
    WHERE ([p].[Id] = [c].[ParentId]) AND[c].[SomeNullableDateTime] IS NULL) AND EXISTS(
   SELECT 1
   FROM[Children] AS [c0]
    WHERE ([p].[Id] = [c0].[ParentId]) AND[c0].[SomeNullableDateTime] IS NULL)
GO

所以看起来问题是 SomeOtherNullableDateTime(应该不为空)甚至没有包含在生成的 SQL 的 where 子句中。

更新 3:这是 SQL EF 核心 5(正确)生成的

SELECT (
    SELECT TOP(1) [c].[SomeOtherNullableDateTime]
    FROM [Children] AS [c]
    WHERE ([p].[Id] = [c].[ParentId]) AND [c].[SomeNullableDateTime] IS NULL
    ORDER BY [c].[SomeInteger])
FROM [Parent] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM [Children] AS [c0]
    WHERE ([p].[Id] = [c0].[ParentId]) AND [c0].[SomeNullableDateTime] IS NULL) AND (
    SELECT TOP(1) [c1].[SomeOtherNullableDateTime]
    FROM [Children] AS [c1]
    WHERE ([p].[Id] = [c1].[ParentId]) AND [c1].[SomeNullableDateTime] IS NULL
    ORDER BY [c1].[SomeInteger]) IS NOT NULL
GO

虽然它可能是回归,但我建议以有效且更可预测的方式重写查询:

var query =
    from p in Parent
    from c in p.Children
        .Where(c.SomeNullableDateTime == null)
        .OrderBy(c => c.SomeInteger)
        .Take(1)
    where c.SomeOtherNullableDateTime != null
    select c.SomeOtherNullableDateTime;

看起来像 EF Core 6.0 查询翻译错误。如果您使用“更自然”的方式编写此类查询,也会发生同样的情况

var query = db.Set<Parent>()
    .Select(p => p.Children
        .Where(c => c.SomeNullableDateTime == null)
        .OrderBy(c => c.SomeInteger)
        .FirstOrDefault())
    .Where(c => c.SomeOtherNullableDateTime != null)
    .Select(c => c.SomeOtherNullableDateTime);

生成的SQL

SELECT (
    SELECT TOP(1) [c0].[SomeOtherNullableDateTime]
    FROM [Child] AS [c0]
    WHERE ([p].[Id] = [c0].[ParentId]) AND [c0].[SomeNullableDateTime] IS NULL
    ORDER BY [c0].[SomeInteger])
FROM [Parent] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM [Child] AS [c]
    WHERE ([p].[Id] = [c].[ParentId]) AND [c].[SomeNullableDateTime] IS NULL)

也缺少 IS NOT NULL 条件,因此您可以在错误报告中包含此场景。

等效模式(使用 SelectMany + Take(1) 而不是 Select + FirstOrDefault()

var query = db.Set<Parent>()
    .SelectMany(p => p.Children
        .Where(c => c.SomeNullableDateTime == null)
        .OrderBy(c => c.SomeInteger)
        .Take(1))
    .Where(c => c.SomeOtherNullableDateTime != null)
    .Select(c => c.SomeOtherNullableDateTime);

(与@Svyatoslav 同时建议的相同),生成不同的 SQL

SELECT [t0].[SomeOtherNullableDateTime]
FROM [Parent] AS [p]
INNER JOIN (
    SELECT [t].[ParentId], [t].[SomeNullableDateTime], [t].[SomeOtherNullableDateTime]
    FROM (
        SELECT [c].[ParentId], [c].[SomeNullableDateTime], [c].[SomeOtherNullableDateTime], ROW_NUMBER() OVER(PARTITION BY [c].[ParentId], [c].[SomeNullableDateTime] ORDER BY [c].[SomeInteger]) AS [row]
        FROM [Child] AS [c]
    ) AS [t]
    WHERE [t].[row] <= 1
) AS [t0] ON ([p].[Id] = [t0].[ParentId]) AND [t0].[SomeNullableDateTime] IS NULL
WHERE [t0].[SomeOtherNullableDateTime] IS NOT NULL

IS NOT NULL 条件,但现在内部子查询看起来不对,因为它 select 每隔 child 按某物排序,然后应用 IS NULL 条件,而 LINQ 查询请求首先应用 IS NULL 条件,然后 select 第一个 child 由某物排序。因此,您也可以将此用例包含在错误报告中。

所有这些查询,包括来自 OP 的查询,都在 EF Core 5.0 中正常工作(生成正确的 SQL)。

GitHub 上的开发团队已确认有两个不同的错误导致了这些问题:

https://github.com/dotnet/efcore/issues/26744

https://github.com/dotnet/efcore/issues/26756

不幸的是,他们表示这些错误不会在计划于 12 月发布的 6.0.1 版本中修复,但最早会在计划于 2022 年 2 月发布的另一个版本中修复。

由于这些错误导致 EF 核心 6 悄悄地 return 错误的结果,并且许多用户很可能会弄乱他们的数据或根据错误的数据做出决定(因为没有人会检查所有 Linq2SQL 查询正确的 SQL 代!?)我建议暂时不要使用 EF core 6!

这可能被认为是基于意见的,但请不要删除此答案,而是将其留作对其他开发人员的警告!

更新: 现在有针对这些问题的修复:

https://github.com/dotnet/efcore/pull/27284

https://github.com/dotnet/efcore/pull/27292

它们已获准与计划于 2022 年 3 月发布的版本 6.0.3 一起发布。