尽管 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 一起发布。
我有一个这样的 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 一起发布。