[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
- 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);
}
}
}
}
}