EF Core DbUpdateConcurrencyException无法按预期工作 [英] EF Core DbUpdateConcurrencyException does not work as expected

查看:86
本文介绍了EF Core DbUpdateConcurrencyException无法按预期工作的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have the following code that I am trying to update ClientAccount using ef core but they Concurrency Check fails:

    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属性标记该属性即可完成工作。

Problem is that DbUpdateConcurrencyException is not fired whenever two threads are trying to update it at the same time and thus I don't have expected functionality. I don't understand what is the problem here as marking the property with ConcurrencyCheck attribute should do the work.

推荐答案


不能按预期工作

does not work as expected

可以,但是您的代码几乎不会引起并发异常。

Sure it does, but your code will hardly ever give rise to concurrency exceptions.

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

In the Update method an existing client is pulled from the database, modified and immediately saved. When coming freshly from the database, the client (obviously) has the latest value of Balance, not the value it had when it entered the UI. The whole operation is a question of milliseconds, small chance that other users save the same client in that short time span.

如果要显示并发冲突,则应将原始值存储在 ClientAccount 对象中,并将其分配给上下文中的原始值。例如:

If you want concurrency conflicts to show up you should store the original value in the ClientAccount object and assign it to the original value in the context. For example like so:

类:

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

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

In the update method, for brevity pretending we have the context available there:

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 由用户。并且由于使用了存储库,因此必须添加一种将原始值提供给包装后的上下文的方法。

You also need to set OriginalBalance in the object that is edited by the user. And since you work with repositories you have to add a method that will feed original values to the wrapped context.

现在全部这仅用于一个物业。更常见的是使用一种特殊的属性进行乐观并发控制,即版本控制。属性-或数据库中的字段。某些数据库(其中是Sql Server)在每次更新时都会自动增加此版本字段,这意味着当记录的 any 值已更新时,此版本字段将始终是不同的。

Now all this was for only one property. It is more common to use one special property for optimistic concurrency control, a "version" property --or field in the database. Some databases (among which Sql Server) auto-increment this version field on each update, which means that it will always be different when any value of a record has been updated.

所以让您上课具有以下属性:

So let you class have this property:

public byte[] Rowversion { get; set; }

和映射:

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

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

现在,无需存储原始余额并在以后使用,您只需......

Now instead of storing the original balance and using it later, you can simply do ...

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

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

... and users will be made aware of any concurrency conflict.

您可以在EF核心此处,但请注意(令人惊讶的是)他们错误地使用了 IsConcurrencyToken()而不是 IsRowVersion 。正如我在此处所述,对于EF6,这会导致不同的行为,但对于EF核心仍然适用。

You can read more on concurrency control in EF-core here, but note that (surprisingly) they incorrectly use IsConcurrencyToken() instead of IsRowVersion. This causes different behavior as I described here for EF6, but it still holds for 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:

This is the executed SQL for the last update:

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

这篇关于EF Core DbUpdateConcurrencyException无法按预期工作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆