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 数据类型