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; } }
具体的做法就是设置一列为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
- catch the dbupdateConcurrencyException during SaveChange.
- Use dbupdateConcurrencyException.Entities to prepare a new set of changes for the affected entities.
- Refresh the original values of the concurrency token to reflect the current value in the database.
- 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); } } } } }