【问题标题】:Is this request generated by EF Core buggy or is it my code?这个请求是由 EF Core 错误生成的还是我的代码?
【发布时间】:2021-08-20 08:47:18
【问题描述】:

简介

我将 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 &gt;= start &amp;&amp; Moment &lt;= 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&lt;Foo&gt; 查询数据:

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)
        //...
}
Function Expected number of records Records from DB Records when using LINQ
Query1 3 3 3
Query2 3 2 3
Query3 3 3 3

为什么第二个数据库查询只返回2条记录?

我知道第一个和第三个查询将DateTimeDateTime 进行比较,而第二个查询将DateTimeDateTimeOffset 进行比较。因此,我想知道为什么 LINQ 版本的行为不同。

跟踪查询

我跟踪了发送到 SQL Server 的实际查询,它们是不同的,但是,我真的不明白为什么它们会导致不同的结果(没有太多 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.EntityFrameworkCoreMicrosoft.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();
        }
    }
}

【问题讨论】:

  • TBH 在我看来很合适。
  • 看起来如果您尝试重新定义两个参数但它不起作用。你应该得到一个错误。不允许在方法中重新定义参数。代码忽略了更改: start = start.Date;和结束=结束。日期;所以你的编译器选项设置为忽略警告而不是报告警告。
  • @jdweng 这是一个赋值,没有重新定义(警告被视为警告级别 5 的错误)。 .Date 生成 DateTime,然后将其转换为 DateTimeOffset。 (应该提到我使用 .NET 5)你是什么意思 “代码忽略了更改:start = start.Date; and end = end.Date;”
  • 除非参数定义为 out 或 ref,否则不应更改方法内的参数值。
  • @jdweng 请提供来源。重新分配变量是非常基本的事情,不会造成麻烦。

标签: c# sql-server datetime entity-framework-core datetimeoffset


【解决方案1】:

第二个和其他两个 LINQ 查询之间的区别(因此不同的翻译 - CAST ... as datetimeoffset)是其他使用 DateTime 比较,而这使用 DateTimeOffset 比较。因为当start类型为DateTimeOffset时,表达式

foo.Moment.Date >= start

由 CLR 类型系统规则(适用于 C# 生成的表达式树)实际上是

((DateTimeOffset)foo.Moment.Date) >= start

foo.Moment.Date &lt;= end 也一样。

现在是两个执行上下文之间的DateTimeDateTimeOffset 转换的区别——客户端(CLR)和服务器(本例中为SqlServer):

CLR

此方法等效于DateTimeOffset 构造函数。生成的DateTimeOffset 对象的偏移量取决于dateTime 参数的DateTime.Kind 属性的值:

  • 如果DateTime.Kind属性的值为DateTimeKind.Utc,则DateTimeOffset对象的日期和时间设置为等于dateTime,其Offset属性设置为等于0。

  • 如果DateTime.Kind属性的值为DateTimeKind.LocalDateTimeKind.Unspecified,则DateTimeOffset对象的日期和时间设置为等于dateTime,其Offset属性设置为等于本地系统当前时区的偏移量。

SqlServer

为了转换为datetimeoffset(n),复制日期,并将时间设置为 00:00.0000000 +00:00

现在您应该清楚地看到差异。 CLR Date 属性返回 DateTimeInspecified 种类,然后使用您本地系统的当前时区偏移量转换回 DateTimeOffset - 根据您初始化的方式很可能 +2变量。在 SQL 查询中,列值转换为 0 偏移量。并且由于DateTimeOffset比较使用所有成员(包括偏移量),因此对于某些数据值,您可以获得不同的结果。

当然,在 LINQ to Objects 上下文中运行相同的程序时,它只是使用 CLR 规则在本地编译和执行,因此没有区别。

简而言之,永远不要依赖非 LINQ to Objects 查询中的转换。使用第一个查询中的显式运算符/方法,或第三个查询中的更正数据类型变量,以确保您比较相同的数据类型,没有隐藏和不确定的转换。

【讨论】:

    猜你喜欢
    • 2011-05-21
    • 1970-01-01
    • 1970-01-01
    • 2019-07-22
    • 2014-09-21
    • 1970-01-01
    • 2017-09-28
    • 1970-01-01
    • 2012-09-08
    相关资源
    最近更新 更多