一个非常奇怪的DbUpdateConcurrencyException案例

时间:2015-10-15 13:21:12

标签: sql-server entity-framework ef-code-first calculated-columns ef-migrations

今天我在代码中找到了一个例外。

DbUpdateConcurrencyException

这非常令人困惑,因为业务逻辑没有变化,也没有实体更新。经过几个小时的尝试,我决定打开SQL Server分析器。

这是我发现的问题的一个例子。 让我们看看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; }

Bellow是迁移中创建列的代码。

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

更新数据库。现在代码抛出DbUpdateConcurrencyException。这就是原因。如果我们查看SQL Server探查器,我们将看到上面的代码:

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

现在查询返回[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))。
  3. 在我的情况下,我先选择,因为我不想更改数据库架构。

    Here是重现问题的项目。

    PS:对不起我的英文:)

3 个答案:

答案 0 :(得分:1)

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

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;
}

恕我直言,你可以使用舍入到ms。

答案 1 :(得分:1)

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

我认为您错过了提及实体的密钥包含SomeIdCreatedAt

答案 2 :(得分:-1)

这是我发现的问题的一个例子。 让我们看看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; }

Bellow是迁移中创建列的代码。

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

更新数据库。现在代码抛出DbUpdateConcurrencyException。这就是原因。如果我们查看SQL Server探查器,我们将看到上面的代码:

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

现在查询返回[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))。
  3. 在我的情况下,我先选择,因为我不想更改数据库架构。

    Here是重现问题的项目。

    PS:对不起我的英文:)