一个非常奇怪的 DbUpdateConcurrencyException 案例

A very strange case of DbUpdateConcurrencyException

今天我在我的代码中发现了一个异常。

DbUpdateConcurrencyException

非常混乱,因为业务逻辑没有变化,实体也没有更新。经过几个小时的理解后,我决定打开 SQL 服务器分析器。

这是我发现的一个问题示例。 让我们看看 Foo 实体定义:

public class Foo
{
    public Foo()
    {
    }

    public Foo(int someId, DateTime createdAt, int x, int y)
    {
        SomeId = someId;
        CreatedAt = createdAt;
        X = x;
        Y = y;
    }

    public int SomeId { get; private set; }

    public DateTime CreatedAt { get; private set; }

    public int X { get; private set; }

    public int Y { get; private set; }
}

如果您尝试添加 Foo 的实例并保存数据库上下文,它将起作用。

using (var myDbContext = new MyDbContext())
{
    DateTime now = DateTime.UtcNow;
    var foo = new Foo(1, now, 2, 2);
    myDbContext.Set<Foo>().Add(foo);
    myDbContext.SaveChanges();
}

下面是保存更改时执行的代码。

exec sp_executesql N'INSERT [dbo].[Foo]([SomeId], [CreatedAt], [X], [Y])
VALUES (@0, @1, @2, @3)
',N'@0 int,@1 datetime2(7),@2 int,@3 int',@0=1,@1='2015-10-15 12:45:15.2580302',@2=2,@3=2

现在让我们添加一个计算列。

    public int Sum { get; private set; }

下面是迁移中创建列的代码。

Sql("alter table dbo.Foo add Sum as (X + Y)");

更新数据库。现在代码抛出 DbUpdateConcurrencyException。这就是为什么。如果我们查看 SQL 服务器分析器,我们会看到上面的代码:

exec sp_executesql N'INSERT [dbo].[Foo]([SomeId], [CreatedAt], [X], [Y])
VALUES (@0, @1, @2, @3)
SELECT [Sum]
FROM [dbo].[Foo]
WHERE @@ROWCOUNT > 0 AND [SomeId] = @0 AND [CreatedAt] = @1',N'@0 int,@1 datetime2(7),@2 int,@3 int',@0=1,@1='2015-10-15 12:55:29.4479727',@2=2,@3=2

现在查询returns[Sum]列,因为它是计算出来的。插入效果很好。但是结果集是空的。我认为它会导致 DbUpdateConcurrencyException。问题出在@1 变量的类型中。它是 datetime2(7),但是如果您查看 CreatedAt 列的类型,您会看到它是 datetime。如果执行上面的脚本,您会在 CreatedAt 列中找到一个包含“2015-10-15 12:55:29.447”的新行(转换工作正常)。但是查询试图找到 CreatedAt 等于 '2015-10-15 12:55:29.4479727' 的行的 [Sum]。结果集当然是空的。


因此,您可以通过两种方式解决问题:

  1. 更改列值的精度(例如没有毫秒)。
  2. 在迁移中手动设置 CreatedAt 列的类型 (datetime2(7))。

在我的例子中,我首先 select 因为我不想更改数据库架构。

Here是重现问题的项目。

PS:对不起我的英语:)

这是我发现的一个问题示例。 让我们看看 Foo 实体定义:

public class Foo
{
    public Foo()
    {
    }

    public Foo(int someId, DateTime createdAt, int x, int y)
    {
        SomeId = someId;
        CreatedAt = createdAt;
        X = x;
        Y = y;
    }

    public int SomeId { get; private set; }

    public DateTime CreatedAt { get; private set; }

    public int X { get; private set; }

    public int Y { get; private set; }
}

如果您尝试添加 Foo 的实例并保存数据库上下文,它将起作用。

using (var myDbContext = new MyDbContext())
{
    DateTime now = DateTime.UtcNow;
    var foo = new Foo(1, now, 2, 2);
    myDbContext.Set<Foo>().Add(foo);
    myDbContext.SaveChanges();
}

下面是保存更改时执行的代码。

exec sp_executesql N'INSERT [dbo].[Foo]([SomeId], [CreatedAt], [X], [Y])
VALUES (@0, @1, @2, @3)
',N'@0 int,@1 datetime2(7),@2 int,@3 int',@0=1,@1='2015-10-15 12:45:15.2580302',@2=2,@3=2

现在让我们添加一个计算列。

    public int Sum { get; private set; }

下面是迁移中创建列的代码。

Sql("alter table dbo.Foo add Sum as (X + Y)");

更新数据库。现在代码抛出 DbUpdateConcurrencyException。这就是为什么。如果我们查看 SQL 服务器分析器,我们会看到上面的代码:

exec sp_executesql N'INSERT [dbo].[Foo]([SomeId], [CreatedAt], [X], [Y])
VALUES (@0, @1, @2, @3)
SELECT [Sum]
FROM [dbo].[Foo]
WHERE @@ROWCOUNT > 0 AND [SomeId] = @0 AND [CreatedAt] = @1',N'@0 int,@1 datetime2(7),@2 int,@3 int',@0=1,@1='2015-10-15 12:55:29.4479727',@2=2,@3=2

现在查询returns[Sum]列,因为它是计算出来的。插入效果很好。但是结果集是空的。我认为它会导致 DbUpdateConcurrencyException。问题出在@1 变量的类型中。它是 datetime2(7),但是如果您查看 CreatedAt 列的类型,您会看到它是 datetime。如果您执行上面的脚本,您将在 CreatedAt 列中找到一个包含“2015-10-15 12:55:29.447”的新行(转换工作正常)。但是查询试图找到 CreatedAt 等于 '2015-10-15 12:55:29.4479727' 的行的 [Sum]。结果集当然是空的。


所以,您可以通过两种方式解决问题:

  1. 更改列值的精度(例如没有毫秒)。
  2. 在迁移中手动设置 CreatedAt 列的类型 (datetime2(7))。

在我的例子中,我首先 select 因为我不想更改数据库架构。

Here是重现问题的项目。

PS:对不起我的英语:)

面对非常相似的情况,我使用以下扩展方法

public static DateTime RoundedToSeconds(this DateTime dt) {
    return new DateTime(dt.Ticks - (dt.Ticks % TimeSpan.TicksPerSecond), dt.Kind);
}

那么你的代码应该是

public Foo(int someId, DateTime createdAt, int x, int y)
{
    SomeId = someId;
    CreatedAt = createdAt.RoundedToSeconds();
    X = x;
    Y = y;
}

恕我直言,您可以四舍五入到毫秒。

此问题不会在 Entity Framework Core 的当前版本中重现。 CreatedAt 的参数类型变为 DateTime2 类型。

我想你没有提到实体的键同时包含 SomeIdCreatedAt