[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.ide

其原理實際上是 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?ui

ef core 的併發衝突解決方案

兩個方案去檢測是否併發衝突:code

  • 將已存在的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; }
    }

或者orm

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.token

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:ci

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);
                }
            }
        }
    }
}
相關文章
相關標籤/搜索