这个请求是由 EF Core buggy 生成的还是我的代码?
Is this request generated by EF Core buggy or is it my code?
简介
我将 EF Core 与 .NET 5.0 和 SQL Server Express 结合使用。基本上,我想知道它是否生成了有问题的 SQL 查询,或者我的代码是否有问题(可能是 :D)。我在问题的底部提供了一个 mre,但希望问题从我收集的数据中变得明显(我已经问过类似的问题,但觉得它需要彻底检修)
设置
我有一个记录和一个 DbContext
如下所示。它被简化为重要的 属性 Moment
,其中 必须 属于 DateTimeOffset
类型(公司指南)。
private class Foo
{
public int ID { get; set; }
public DateTimeOffset Moment { get; set; }
}
private class Context : DbContext
{
public Context(DbContextOptions<Context> options) : base(options) {}
public DbSet<Foo> Foos { get; set; }
}
生成的数据库中的相应列的数据类型为 datetimeoffset(7)
,看起来不错。
我用这些数据初始化了数据库(连续 5 天,每天在各自时区的午夜):
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-21 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-22 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-23 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-24 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-25 00:00 +02:00"), });
现在我想查询所有记录 Moment >= start && Moment <= end
而忽略这些参数的时间:
var start = DateTimeOffset.Parse("2021-04-22 00:00 +02:00");
var end = DateTimeOffset.Parse("2021-04-24 00:00 +02:00");
我希望得到 3 条记录并提出了 3 个与我看起来相同的查询,但是,第二个产生了不同的结果:
查询
private static async Task Query1(Context context, DateTimeOffset start, DateTimeOffset end)
{
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
//... Finds 3 records, expected
}
private static async Task Query2(Context context, DateTimeOffset start, DateTimeOffset end)
{
start = start.Date; // .Date yields DateTime -> implicit conversion to DateTimeOffset?
end = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start && foo.Moment.Date <= end)
// ... Finds only 2 records, unexpected
}
private static async Task Query3(Context context, DateTimeOffset start, DateTimeOffset end)
{
var start2 = start.Date; // start2 and end2 are of type DateTime now
var end2 = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start2 && foo.Moment.Date <= end2)
// ... Finds 3 records, expected
}
结果
我还为每个查询制作了一个 LINQ 版本,其中我从 List<Foo>
:
查询数据
private static void Query1(List<Foo> foos, DateTimeOffset start, DateTimeOffset end)
{
var records = foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
//...
}
函数
预期记录数
来自数据库的记录
使用LINQ时的记录
查询 1
3
3
3
查询2
3
2
3
查询3
3
3
3
为什么第二个数据库只查询了return2条记录?
我知道第一个和第三个查询将 DateTime
与 DateTime
进行比较,而第二个查询将 DateTime
与 DateTimeOffset
进行比较。因此我想知道为什么 LINQ 版本的行为不同。
跟踪查询
我跟踪了发送到 SQL 服务器的实际查询,它们是不同的,但是,我真的不明白为什么它们会导致不同的结果(SQL 经验不多) :
-- From Query1()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CONVERT(date, [f].[Moment]) >= @__start_Date_0) AND (CONVERT(date, [f].[Moment]) <= @__end_Date_1)',N'@__start_Date_0 datetime2(7),@__end_Date_1 datetime2(7)',@__start_Date_0='2021-04-22 00:00:00',@__end_Date_1='2021-04-24 00:00:00'
-- From Query2()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CAST(CONVERT(date, [f].[Moment]) AS datetimeoffset) >= @__start_0) AND (CAST(CONVERT(date, [f].[Moment]) AS datetimeoffset) <= @__end_1)',N'@__start_0 datetimeoffset(7),@__end_1 datetimeoffset(7)',@__start_0='2021-04-22 00:00:00 +02:00',@__end_1='2021-04-24 00:00:00 +02:00'
-- From Query3()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CONVERT(date, [f].[Moment]) >= @__start2_0) AND (CONVERT(date, [f].[Moment]) <= @__end2_1)',N'@__start2_0 datetime2(7),@__end2_1 datetime2(7)',@__start2_0='2021-04-22 00:00:00',@__end2_1='2021-04-24 00:00:00'
MRE
使用 Microsoft.EntityFrameworkCore
和 Microsoft.EntityFrameworkCore.SqlServer
版本 5.0.6
进行测试。
namespace Playground
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
public static class Program
{
private class Foo
{
public int ID { get; set; }
public DateTimeOffset Moment { get; set; }
}
private static Context CreateContext()
{
var connectionString = $"Data Source=.\SQLEXPRESS;Initial Catalog=FOO_DB;Integrated Security=SSPI";
var optionsBuilder = new DbContextOptionsBuilder<Context>();
optionsBuilder.UseSqlServer(connectionString).EnableSensitiveDataLogging();
var context = new Context(optionsBuilder.Options);
context.Database.EnsureCreated();
return context;
}
private class Context : DbContext
{
public Context(DbContextOptions<Context> options) : base(options) { }
public DbSet<Foo> Foos { get; set; }
}
private static async Task Query1(Context context, DateTimeOffset start, DateTimeOffset end)
{
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query1 in DB found {records.Count} records");
}
private static async Task Query2(Context context, DateTimeOffset start, DateTimeOffset end)
{
start = start.Date;
end = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start && foo.Moment.Date <= end)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query2 in DB found {records.Count} records");
}
private static async Task Query3(Context context, DateTimeOffset start, DateTimeOffset end)
{
var start2 = start.Date;
var end2 = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start2 && foo.Moment.Date <= end2)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query3 in DB found {records.Count} records");
}
public async static Task Main()
{
var context = CreateContext();
var foos = new List<Foo>() {
new Foo() { Moment = DateTimeOffset.Parse("2021-04-21 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-22 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-23 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-24 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-25 00:00 +02:00"), },
};
if (!context.Foos.Any())
{
await context.AddRangeAsync(foos);
}
await context.SaveChangesAsync();
var start = DateTimeOffset.Parse("2021-04-22 00:00 +02:00");
var end = DateTimeOffset.Parse("2021-04-24 00:00 +02:00");
await Query1(context, start, end);
await Query2(context, start, end);
await Query3(context, start, end);
context.Dispose();
}
}
}
第二个和其他两个 LINQ 查询之间的区别(因此翻译不同 - CAST ... as datetimeoffset
)是其他使用 DateTime
比较,而这个使用 DateTimeOffset
比较。因为当start
类型为DateTimeOffset
时,表达式
foo.Moment.Date >= start
根据 CLR 类型系统规则(适用于 C# 生成的表达式树)实际上是
((DateTimeOffset)foo.Moment.Date) >= start
与 foo.Moment.Date <= end
相同。
现在是两个执行上下文 - 客户端 (CLR) 和服务器(在本例中为 SqlServer)之间 DateTime
到 DateTimeOffset
转换的区别:
This method is equivalent to the DateTimeOffse
t constructor. The offset of the resulting DateTimeOffset
object depends on the value of the DateTime.Kind
property of the dateTime
parameter:
If the value of the DateTime.Kind
property is DateTimeKind.Utc
, the date and time of the DateTimeOffset
object is set equal to dateTime
, and its Offset property is set equal to 0.
If the value of the DateTime.Kind
property is DateTimeKind.Local
or DateTimeKind.Unspecified
, the date and time of the DateTimeOffset
object is set equal to dateTime
, and its Offset
property is set equal to the offset of the local system's current time zone.
For conversion to datetimeoffset(n)
, date is copied, and the time is set to 00:00.0000000 +00:00
现在你应该清楚地看出区别了。 CLR Date
属性 returns DateTime
与 Inspecified
类型,然后使用您的 本地系统的当前类型转换回 DateTimeOffset
时区偏移量 - 根据您初始化变量的方式,很可能是+2。在 SQL 查询中,列值转换为 0 偏移量。由于 DateTimeOffset
比较使用所有成员(包括偏移量),因此对于某些数据值可能会得到不同的结果。
当然,当运行在LINQ to Objects上下文中相同时,它只是使用CLR规则在本地编译和执行,因此没有区别。
简而言之,永远不要依赖非 LINQ to Objects 查询中的转换。在第一个查询中使用显式 operators/methods,或在第三个查询中更正数据类型变量,以确保您比较的是相同数据类型,没有隐藏和不确定的转换。
简介
我将 EF Core 与 .NET 5.0 和 SQL Server Express 结合使用。基本上,我想知道它是否生成了有问题的 SQL 查询,或者我的代码是否有问题(可能是 :D)。我在问题的底部提供了一个 mre,但希望问题从我收集的数据中变得明显(我已经问过类似的问题,但觉得它需要彻底检修)
设置
我有一个记录和一个 DbContext
如下所示。它被简化为重要的 属性 Moment
,其中 必须 属于 DateTimeOffset
类型(公司指南)。
private class Foo
{
public int ID { get; set; }
public DateTimeOffset Moment { get; set; }
}
private class Context : DbContext
{
public Context(DbContextOptions<Context> options) : base(options) {}
public DbSet<Foo> Foos { get; set; }
}
生成的数据库中的相应列的数据类型为 datetimeoffset(7)
,看起来不错。
我用这些数据初始化了数据库(连续 5 天,每天在各自时区的午夜):
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-21 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-22 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-23 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-24 00:00 +02:00"), });
context.Foos.Add(new Foo() { Moment = DateTimeOffset.Parse("2021-04-25 00:00 +02:00"), });
现在我想查询所有记录 Moment >= start && Moment <= end
而忽略这些参数的时间:
var start = DateTimeOffset.Parse("2021-04-22 00:00 +02:00");
var end = DateTimeOffset.Parse("2021-04-24 00:00 +02:00");
我希望得到 3 条记录并提出了 3 个与我看起来相同的查询,但是,第二个产生了不同的结果:
查询
private static async Task Query1(Context context, DateTimeOffset start, DateTimeOffset end)
{
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
//... Finds 3 records, expected
}
private static async Task Query2(Context context, DateTimeOffset start, DateTimeOffset end)
{
start = start.Date; // .Date yields DateTime -> implicit conversion to DateTimeOffset?
end = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start && foo.Moment.Date <= end)
// ... Finds only 2 records, unexpected
}
private static async Task Query3(Context context, DateTimeOffset start, DateTimeOffset end)
{
var start2 = start.Date; // start2 and end2 are of type DateTime now
var end2 = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start2 && foo.Moment.Date <= end2)
// ... Finds 3 records, expected
}
结果
我还为每个查询制作了一个 LINQ 版本,其中我从 List<Foo>
:
private static void Query1(List<Foo> foos, DateTimeOffset start, DateTimeOffset end)
{
var records = foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
//...
}
函数 | 预期记录数 | 来自数据库的记录 | 使用LINQ时的记录 |
---|---|---|---|
查询 1 | 3 | 3 | 3 |
查询2 | 3 | 2 | 3 |
查询3 | 3 | 3 | 3 |
为什么第二个数据库只查询了return2条记录?
我知道第一个和第三个查询将 DateTime
与 DateTime
进行比较,而第二个查询将 DateTime
与 DateTimeOffset
进行比较。因此我想知道为什么 LINQ 版本的行为不同。
跟踪查询
我跟踪了发送到 SQL 服务器的实际查询,它们是不同的,但是,我真的不明白为什么它们会导致不同的结果(SQL 经验不多) :
-- From Query1()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CONVERT(date, [f].[Moment]) >= @__start_Date_0) AND (CONVERT(date, [f].[Moment]) <= @__end_Date_1)',N'@__start_Date_0 datetime2(7),@__end_Date_1 datetime2(7)',@__start_Date_0='2021-04-22 00:00:00',@__end_Date_1='2021-04-24 00:00:00'
-- From Query2()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CAST(CONVERT(date, [f].[Moment]) AS datetimeoffset) >= @__start_0) AND (CAST(CONVERT(date, [f].[Moment]) AS datetimeoffset) <= @__end_1)',N'@__start_0 datetimeoffset(7),@__end_1 datetimeoffset(7)',@__start_0='2021-04-22 00:00:00 +02:00',@__end_1='2021-04-24 00:00:00 +02:00'
-- From Query3()
exec sp_executesql N'SELECT [f].[ID]
FROM [Foos] AS [f]
WHERE (CONVERT(date, [f].[Moment]) >= @__start2_0) AND (CONVERT(date, [f].[Moment]) <= @__end2_1)',N'@__start2_0 datetime2(7),@__end2_1 datetime2(7)',@__start2_0='2021-04-22 00:00:00',@__end2_1='2021-04-24 00:00:00'
MRE
使用 Microsoft.EntityFrameworkCore
和 Microsoft.EntityFrameworkCore.SqlServer
版本 5.0.6
进行测试。
namespace Playground
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
public static class Program
{
private class Foo
{
public int ID { get; set; }
public DateTimeOffset Moment { get; set; }
}
private static Context CreateContext()
{
var connectionString = $"Data Source=.\SQLEXPRESS;Initial Catalog=FOO_DB;Integrated Security=SSPI";
var optionsBuilder = new DbContextOptionsBuilder<Context>();
optionsBuilder.UseSqlServer(connectionString).EnableSensitiveDataLogging();
var context = new Context(optionsBuilder.Options);
context.Database.EnsureCreated();
return context;
}
private class Context : DbContext
{
public Context(DbContextOptions<Context> options) : base(options) { }
public DbSet<Foo> Foos { get; set; }
}
private static async Task Query1(Context context, DateTimeOffset start, DateTimeOffset end)
{
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start.Date && foo.Moment.Date <= end.Date)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query1 in DB found {records.Count} records");
}
private static async Task Query2(Context context, DateTimeOffset start, DateTimeOffset end)
{
start = start.Date;
end = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start && foo.Moment.Date <= end)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query2 in DB found {records.Count} records");
}
private static async Task Query3(Context context, DateTimeOffset start, DateTimeOffset end)
{
var start2 = start.Date;
var end2 = end.Date;
var records = await context.Foos
.Where(foo => foo.Moment.Date >= start2 && foo.Moment.Date <= end2)
.Select(foo => foo.ID)
.ToListAsync();
Console.WriteLine($"Query3 in DB found {records.Count} records");
}
public async static Task Main()
{
var context = CreateContext();
var foos = new List<Foo>() {
new Foo() { Moment = DateTimeOffset.Parse("2021-04-21 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-22 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-23 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-24 00:00 +02:00"), },
new Foo() { Moment = DateTimeOffset.Parse("2021-04-25 00:00 +02:00"), },
};
if (!context.Foos.Any())
{
await context.AddRangeAsync(foos);
}
await context.SaveChangesAsync();
var start = DateTimeOffset.Parse("2021-04-22 00:00 +02:00");
var end = DateTimeOffset.Parse("2021-04-24 00:00 +02:00");
await Query1(context, start, end);
await Query2(context, start, end);
await Query3(context, start, end);
context.Dispose();
}
}
}
第二个和其他两个 LINQ 查询之间的区别(因此翻译不同 - CAST ... as datetimeoffset
)是其他使用 DateTime
比较,而这个使用 DateTimeOffset
比较。因为当start
类型为DateTimeOffset
时,表达式
foo.Moment.Date >= start
根据 CLR 类型系统规则(适用于 C# 生成的表达式树)实际上是
((DateTimeOffset)foo.Moment.Date) >= start
与 foo.Moment.Date <= end
相同。
现在是两个执行上下文 - 客户端 (CLR) 和服务器(在本例中为 SqlServer)之间 DateTime
到 DateTimeOffset
转换的区别:
This method is equivalent to the
DateTimeOffse
t constructor. The offset of the resultingDateTimeOffset
object depends on the value of theDateTime.Kind
property of thedateTime
parameter:
If the value of the
DateTime.Kind
property isDateTimeKind.Utc
, the date and time of theDateTimeOffset
object is set equal todateTime
, and its Offset property is set equal to 0.If the value of the
DateTime.Kind
property isDateTimeKind.Local
orDateTimeKind.Unspecified
, the date and time of theDateTimeOffset
object is set equal todateTime
, and itsOffset
property is set equal to the offset of the local system's current time zone.
For conversion to
datetimeoffset(n)
, date is copied, and the time is set to 00:00.0000000 +00:00
现在你应该清楚地看出区别了。 CLR Date
属性 returns DateTime
与 Inspecified
类型,然后使用您的 本地系统的当前类型转换回 DateTimeOffset
时区偏移量 - 根据您初始化变量的方式,很可能是+2。在 SQL 查询中,列值转换为 0 偏移量。由于 DateTimeOffset
比较使用所有成员(包括偏移量),因此对于某些数据值可能会得到不同的结果。
当然,当运行在LINQ to Objects上下文中相同时,它只是使用CLR规则在本地编译和执行,因此没有区别。
简而言之,永远不要依赖非 LINQ to Objects 查询中的转换。在第一个查询中使用显式 operators/methods,或在第三个查询中更正数据类型变量,以确保您比较的是相同数据类型,没有隐藏和不确定的转换。