[entity framework core] Concurrency Management in Entity Framework Core

https://www.learnentityframeworkcore.com/concurrency

concurrency token

无论何时, 当一个更新操作或者删除操作发生时, 即我们的代码调用 SaveChange() 时, target value 上面拿到当前的 concurrency token, 与调用 Save Change 之前记录的 concurrency token 相比较.
- 如果两者 match, 那么此次 operation 将会执行.
- 如果两者不 match, 那么 EF Core 会认为有其他的 operation 在对 target value 进行操作. 则会放弃此次 operation.

其原理其实是 EF Core 会在每个 update / delete 语句的 where 条件中增添一个 statement: where xxx = yyy and concurrency token = old token, 随后 ef core 会记录此次 operation affect 的行数, 如果行数为 0 那么会抛出一个 DbUpdateConcurrencyException. but the exception will be informed to the user. what is the next step?

ef core 的并发冲突解决方案

两个方案去检测是否并发冲突:

  • 将已存在的properties配置成为concurrency token
    public class Author
    {
        public int AuthorId { get; set; }
        public string FirstName { get; set; }
        [ConcurrencyCheck]
        public string LastName { get; set; }
        public ICollection<Book> Books { get; set; }
    }

或者

    public class SampleContext : DbContext
    {
        public DbSet<Author> Authors { get; set; }
        
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
             modelBuilder.Entity<Author>()
                .Property(a => a.LastName).IsConcurrencyToken();
        } 
    }
    public class Author
    {
        public int AuthorId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public ICollection<Book> Books { get; set; }
    }
  • 添加一个额外的代表 "row version"的属性去作为concurrency token

具体的做法就是设置一列为version, 自增列, 当user a, 和 user b 同时获取到这个数据时, version为0, 此时 user a 对数据进行了修改, 存回到db中, 此时version为1, user b对数据也进行了修改, 再存入db时发现已经找不到version为0的这行数据了, 此时user b会得到一个 DbUpdateConcurrencyException.

    public class Author
    {
        public int AuthorId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public ICollection<Book> Books { get; set; }
        [TimeStamp]
        public byte[] RowVersion { get; set; }
    }
    public class SampleContext : DbContext
    {
        public DbSet<Author> Authors { get; set; }
        
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
             modelBuilder.Entity<Author>()
                .Property(a => a.RowVersion)
                .IsConcurrencyToken()
                .ValueGeneratedOnAddOrUpdate();
        } 
    }
    public class Author
    {
        public int AuthorId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public ICollection<Book> Books { get; set; }
        public byte[] RowVersion { get; set; }
    }

resolving a concurrency conflict

  1. catch the DbUpdateConcurrencyException during SaveChange.
  2. Use DbUpdateConcurrencyException.Entities to prepare a new set of changes for the affected entities.
  3. Refresh the original values of the concurrency token to reflect the current value in the database.
  4. Retry the process until no conflicts occur.

the example code like below:

using (var context = new PersonContext())
{
    // Fetch a person from database and change phone number
    var person = context.People.Single(p => p.PersonId == 1);
    person.PhoneNumber = "555-555-5555";

    // Change the person's name in the database to simulate a concurrency conflict
    context.Database.ExecuteSqlRaw(
        "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

    var saved = false;
    while (!saved)
    {
        try
        {
            // Attempt to save changes to the database
            context.SaveChanges();
            saved = true;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            foreach (var entry in ex.Entries)
            {
                if (entry.Entity is Person)
                {
                    var proposedValues = entry.CurrentValues;
                    var databaseValues = entry.GetDatabaseValues();

                    foreach (var property in proposedValues.Properties)
                    {
                        var proposedValue = proposedValues[property];
                        var databaseValue = databaseValues[property];

                        // TODO: decide which value should be written to database
                        // proposedValues[property] = <value to be saved>;
                    }

                    // Refresh original values to bypass next concurrency check
                    entry.OriginalValues.SetValues(databaseValues);
                }
                else
                {
                    throw new NotSupportedException(
                        "Don't know how to handle concurrency conflicts for "
                        + entry.Metadata.Name);
                }
            }
        }
    }
}
posted @ 2019-10-12 13:26  YanyuWu  阅读(230)  评论(0编辑  收藏  举报