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

Posted

技术标签:

【中文标题】EF 核心 6 选择空值,尽管 where 子句要求不为空【英文标题】:EF core 6 selecting null values despite where clause asking for not null 【发布时间】:2021-12-29 05:02:17 【问题描述】:

我有一个这样的 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 core 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

【问题讨论】:

生成的SQL语句是什么? @StefanGolubović:我已将生成的 sql 语句添加到问题中 我觉得奇怪的是你没有在声明的末尾加上 .First。 为什么是Include,然后是Select?这两件事不能很好地结合在一起(很可能Include 被忽略了)。我只想Select + Where @IvanStoev:我只是想弄清楚你想说什么:-D 【参考方案1】:

看起来像 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 条件,但现在内部子查询看起来是错误的,因为它选择了按某些东西排序的每个第一个子查询,然后应用IS NULL 条件,而LINQ 查询请求首先应用IS NULL 条件,然后选择由某物订购的第一个孩子。所以你也可以在错误报告中包含这个用例。

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

【讨论】:

是的,子查询优化中的严重错误。他们不应该改变窗口搜索条件。 所以我们在一个 SO 问题中发现了两个不同的问题。可能您必须为他们创建新问题。 @Ivan Stoev:我已将您的两个示例添加到 GitHub Ticket (github.com/dotnet/efcore/issues/26744) @Ivan Stoev:开发团队已经确认了与这些查询相关的两个错误。不幸的是,它们最早要到明年二月才能修复。因此,我写了另一个答案作为对其他开发人员的警告。 @Ivan Stoev:我在 GitHub 上询问过这些错误是否仅限于所提供的场景,或者是否还有其他类型的查询受到影响。如果我得到答案,我会将其包含在我的答案中。【参考方案2】:

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

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

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

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

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

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

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

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

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

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

【讨论】:

【参考方案3】:

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

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 核心 6 选择空值,尽管 where 子句要求不为空的主要内容,如果未能解决你的问题,请参考以下文章

EF6 - 在 Where() 子句中使用 await 关键字

ODP.NET / EF6 - WHERE 子句中的 CHAR 数据类型

用于返回结果集的 where 子句中的 case 语句包含空值

WHERE子句不使用空值

EF4 将 is null 子句添加到 where 子句

EF:包含 where 子句 [重复]