【问题标题】:EF Core DbUpdateConcurrencyException does not work as expectedEF Core DbUpdateConcurrencyException 无法按预期工作
【发布时间】:2018-05-03 17:11:44
【问题描述】:

我有以下代码,我正在尝试使用 ef core 更新 ClientAccount,但它们的并发检查失败:

    public class ClientAccount
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public long Id { get; set; }

        [Required]
        [ConcurrencyCheck]
        public double Balance { get; set; }

        [Required]
        public DateTime DateTimeCreated { get; set; }

        [Required]
        public DateTime DateTimeUpdated { get; set; }       
    }

    public class ClientRepository
    {
        private readonly MyContext context;

        public ClientRepository(MyContext context)
        {
            this.context = context;
        }

        public ClientAccount GetClientAccount()
        {
            return (from client in context.ClientAccount
                    select client).SingleOrDefault();
        }

        public void Update(ClientAccount client)
        {
            context.Update(client);
            context.Entry(client).Property(x => x.DateTimeCreated).IsModified = false;
        }
    }

    public class ClientService
    {
        private readonly ClientRepository clientRepository;
        private readonly IUnitOfWork unitOfWork;

        public ClientService(ClientRepository clientRepository,
            IUnitOfWork unitOfWork)
        {
            this.unitOfWork = unitOfWork;
            this.clientRepository = clientRepository;
        }

        public void Update(ClientAccount clientAccount)
        {
            if (clientAccount == null)
                return;

            try
            {
                ClientAccount existingClient = clientRepository.GetClientAccount();
                if (existingClient == null)
                {
                    // COde to create client
                }
                else
                {
                    existingClient.AvailableFunds = clientAccount.Balance;
                    existingClient.DateTimeUpdated = DateTime.UtcNow;

                    clientRepository.Update(existingClient);
                }

                unitOfWork.Commit();
            }
            catch (DbUpdateConcurrencyException ex)
            {

            }
        }
    }

问题是DbUpdateConcurrencyException 不会在两个线程同时尝试更新它时被触发,因此我没有预期的功能。 我不明白这里有什么问题,因为用 ConcurrencyCheck 属性标记属性应该可以完成工作。

【问题讨论】:

  • 有人可以帮忙吗?
  • 在 cmets 中寻求帮助并不是很有用。它不会将问题推回到首页。编辑你的问题。例如添加执行实际修改的代码。这部分对于回答问题至关重要。
  • 添加了完整的代码

标签: c# .net ef-code-first entity-framework-core ef-core-2.0


【解决方案1】:

没有按预期工作

确实如此,但您的代码几乎不会引发并发异常。

Update 方法中,现有客户端从数据库中提取、修改并立即保存。当刚从数据库中来时,客户端(显然)具有最新值Balance,而不是它进入 UI 时的值。整个操作是毫秒级的问题,其他用户在很短的时间内保存同一个客户端的可能性很小。

如何解决

如果您希望显示并发冲突,您应该将原始值存储在 ClientAccount 对象中,并将其分配给上下文中的原始值。比如像这样:

班级:

public class ClientAccount
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    [Required]
    [ConcurrencyCheck]
    public double Balance { get; set; }
    
    [NotMapped]
    public double OriginalBalance { get; set; }
    
    ...
}

在更新方法中,为了简洁起见,假设我们在那里有可用的上下文:

ClientAccount existingClient = db.ClientAccount.Find(clientAccount.Id);

db.Entry(existingClient).OriginalValues["Balance"] = clientAccount.OriginalBalance;

existingClient.Balance = clientAccount.Balance; // assuming that AvailableFunds is a typo
db.SaveChanges();

您还需要在用户编辑的对象中设置OriginalBalance。而且由于您使用存储库,因此您必须添加一个方法,将原始值提供给包装的上下文。

更好的方法?

现在这一切只针对一处房产。更常见的是使用一种特殊属性进行乐观并发控制,即“版本”属性——或数据库中的字段。某些数据库(其中 Sql Server)在每次更新时自动增加此版本字段,这意味着当记录的 任何 值已更新时,它总是不同的。

所以让你类拥有这个属性:

public byte[] Rowversion { get; set; }

还有映射:

modelBuilder.Entity<ClientAccount>().Property(c => c.Rowversion).IsRowVersion();

(或使用[System.ComponentModel.DataAnnotations.Timestamp] 属性)。

现在您不必存储原始余额并在以后使用它,您可以简单地执行...

db.Entry(existingClient).OriginalValues["Rowversion"] = clientAccount.Rowversion;

...并且用户将被告知任何并发冲突。

您可以在 EF-core here 中阅读有关并发控制的更多信息,但请注意(令人惊讶的是)他们错误地使用了 IsConcurrencyToken() 而不是 IsRowVersion。这会导致不同的行为,正如我为 EF6 描述的 here,但它仍然适用于 EF-core。

示例代码

using (var db = new MyContext(connectionString))
{
    var editedClientAccount = db.ClientAccounts.FirstOrDefault();
    editedClientAccount.OrgBalance = editedClientAccount.Balance;
    // Mimic editing in UI:
    editedClientAccount.Balance = DateTime.Now.Ticks;

    // Mimic concurrent update.
    Thread.Sleep(200);
    using (var db2 = new MyContext(connectionString))
    {
        db2.ClientAccounts.First().Balance = DateTime.Now.Ticks;
        db2.SaveChanges();
    }
    Thread.Sleep(200);
    
    // Mimic return from UI:
    var existingClient = db.ClientAccounts.Find(editedClientAccount.ID);
    db.Entry(existingClient).OriginalValues["Balance"] = editedClientAccount.OrgBalance;
    existingClient.Balance = editedClientAccount.Balance;
            
    db.SaveChanges(); // Throws the DbUpdateConcurrencyException
}

这是上次更新执行的 SQL:

exec sp_executesql N'SET NOCOUNT ON;
UPDATE [ClientAccount] SET [Balance] = @p0
WHERE [ID] = @p1 AND [Balance] = @p2;
SELECT @@ROWCOUNT;

',N'@p1 int,@p0 float,@p2 float',@p1=6,@p0=636473049969058940,@p2=1234

【讨论】:

  • 感谢您的建议。我设法通过添加一个 if 条件等待特定的 balance 值,然后是 Thread Sleep 来模拟 ms 场景。线程进入睡眠模式后,我启动一个新线程来更新客户端余额。当第一个线程恢复并尝试更新它不再存在的值时,DbUpdateConcurrencyException 不会触发。我在同一个表中还有十几个其他属性需要检查并发性。不仅仅是余额,只是我在上面发布了一个小示例代码。
  • 正如我在上面的答案中提到的,我想避免 Rowversion 解决方案并使用 ConcurrencyCheck 属性,因为它是 EF 核心提供的功能。谢谢
  • 谢谢。不知道发生了什么,但它现在似乎没有任何问题,没有改变任何东西。我上面的代码似乎是正确的。也许这是测试的线程问题
【解决方案2】:

[Timestamp] 属性添加到DateTimeUpdated。在那之后应该可以工作,但不可否认,我还没有使用过这个功能。我相信类型应该是byte[]

找到参考链接:https://docs.microsoft.com/en-us/ef/core/modeling/concurrency#timestamprow-version

【讨论】:

  • 感谢您的帮助。我想避免这种解决方案并使用 ConcurrencyCheck 属性
猜你喜欢
  • 2019-08-26
  • 2020-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-30
  • 1970-01-01
  • 2020-11-14
  • 1970-01-01
相关资源
最近更新 更多