标签:strong value rri out 提供者 帮助 tco 复制 base
将属性配置为并发令牌来实现乐观并发控制
使用数据注解 ConcurrencyCheckAttribute
将属性配置为并发令牌
public class Person
{
[Key]
public int Id { get; set; }
[ConcurrencyCheck]
[MaxLength(32)]
public string FirstName { get; set; }
[MaxLength(32)]
public string LastName { get; set; }
}
使用 Fluent Api 配置属性为并发令牌
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Person>().Property(s => s.FirstName).IsConcurrencyToken();
}
数据库新增或更新时会生成一个新的值赋予给配置为时间戳的属性,此属性也被视作为并发令牌。这样做可以确保你在查询一行数据后(ChangeTracker),尝试更新此行,但在此时数据已经被其他人修改,会返回一个异常。
使用数据注解 TimestampAttribute
将属性标记为时间戳
public class Person
{
[Key]
public int Id { get; set; }
[ConcurrencyCheck]
[MaxLength(32)]
public string FirstName { get; set; }
[MaxLength(32)]
public string LastName { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
### Fluent Api
使用 Fluent Api 标志属性为时间戳
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Person>()
.Property(s => s.FirstName).IsConcurrencyToken();
builder.Entity<Person>()
.Property(s => s.Timestamp).IsRowVersion();
}
添加迁移
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "Timestamp",
table: "People",
rowVersion: true,
nullable: true);
}
看看 ModelSnapshot
生成的迁移
modelBuilder.Entity("LearningEfCore.Person", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("FirstName")
.IsConcurrencyToken()
.HasMaxLength(32);
b.Property<string>("LastName")
.HasMaxLength(32);
b.Property<byte[]>("Timestamp")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
b.ToTable("People");
});
ValueGeneratedOnAddOrUpdate
正是表示数据在插入与更新时自动生成
配置实体属性为并发令牌来实现乐观并发控制:当更新或者删除操作在 SaveChanges
过程中出现时,EF Core 将会把数据库中并发令牌的值与 ChangeTracker
中跟踪的值进行比较。
其他用户执行的与当前用户相冲突的操作称为并发冲突.
数据库提供者复制实现并发令牌的比较.
在关系型数据库中, EF Core 在更新与删除操作中会通过在 WHERE
条件语句中包含对并发令牌的比较,当语句执行后,EF Core 会读取影响的行数,如果没有任何行受影响,则会检测到并发冲突, EF Core 也会抛出 DbUpdateConcurrencyException
下面我们通过例子来演示一下:
先创建一个 Api 控制器:
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
private readonly TestDbContext _db;
public DefaultController(TestDbContext db)
{
_db = db;
}
[HttpPost]
public async Task<Person> Add([FromBody]Person person)
{
var entry = await _db.People.AddAsync(person);
await _db.SaveChangesAsync();
return entry.Entity;
}
[HttpPut]
public async Task<Person> Update([FromBody]Person current)
{
var original = await _db.People.FindAsync(current.Id);
original.FirstName = current.FirstName;
original.LastName = current.LastName;
await _db.SaveChangesAsync();
return original;
}
}
以 POST 方式请求 http://localhost:5000/api/default
, body 使用 json 串:
{
"firstName": "James",
"lastName": "Rajesh"
}
返回:
{
"id": 1,
"firstName": "James",
"lastName": "Rajesh",
"timestamp": "AAAAAAAAB9E="
}
可以看到 timestamp
如我们所愿,自动生成了一个并发令牌
下面我们尝试在 SaveChanges
时修改数据库中的值:
在 Update
接口中的 await _db.SaveChangesAsync();
此行下断点。
修改 Request Body 为:
{
"id": 1,
"firstName": "James1",
"lastName": "Rajesh1",
"timestamp": "AAAAAAAAB9E="
}
使用 PUT 方式请求 http://localhost:5000/api/default
, 命中断点后,
修改数据库中 LastName
的值为 Rajesh2,然后 F10,我们会得到如下并发异常:
DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
此时有人可能会疑惑了, EF Core 是如何检测到 LastName
变更了的呢? 其实不然, 是我们在修改数据库中数据的时候, RawVersion 列 Timestamp
自动会更新。而且每一次我们使用 EF Core 更新的时候, 产生的语句是这样的(通过控制台日志可以看到):
Executed DbCommand (68ms) [Parameters=[@p2=‘?‘ (DbType = Int32), @p0=‘?‘ (Size = 32), @p3=‘?‘ (Size = 32), @p1=‘?‘ (Size = 32), @p4=‘?‘ (Size = 8) (DbType = Binary)], CommandType=‘Text‘, CommandTimeout=‘30‘] SET NOCOUNT ON; UPDATE [People] SET [FirstName] = @p0, [LastName] = @p1 WHERE [Id] = @p2 AND [FirstName] = @p3 AND [Timestamp] = @p4;
这里会使用 WHERE
条件进行判断 Timestamp
是否一致
下面去掉 Timestamp
列, 留下标志为 ConcurrencyToken
的 FirstName
使用 PUT 方式请求 http://localhost:5000/api/default
, body 为:
{
"id": 1,
"firstName": "James6",
"lastName": "Rajesh11"
}
再来在 SaveChanges
的时候修改数据库中对应记录的 LastName
的值为 Rajesh19 , 此时没报错,返回值为:
{
"id": 1,
"firstName": "James6",
"lastName": "Rajesh11"
}
数据库的值也被修改为 Rajesh11. 说明这里没有检测到并发,下面我们尝试修改 FirstName
为 James12, 同时在 SaveChanges
时修改为 Rajesh13, 此时就检测到了并发冲突, 我们看控制台的语句为:
Executed DbCommand (63ms) [Parameters=[@p1=‘?‘ (DbType = Int32), @p0=‘?‘ (Size = 32), @p2=‘?‘ (Size = 32)], CommandType=‘Text‘, CommandTimeout=‘30‘] SET NOCOUNT ON; UPDATE [People] SET [FirstName] = @p0 WHERE [Id] = @p1 AND [FirstName] = @p2;
看得到这里会判断 FirstName
与 ChangeTracker
中的值进行比较,执行之后没有受影响的行,所以才会检测到并发冲突。
先来了解下帮助解决并发冲突的三组值:
当 SaveChanges
时,如果捕获了 DbUpdateConcurrencyException
, 说明发生了并发冲突,使用 DbUpdateConcurrencyException.Entries
为受影响的实体准备一组新值,重新获取数据库中的值的并发令牌来刷新 Original values
, 然后重试直到没有任何冲突产生。
[HttpPut]
public async Task<Person> Update([FromBody]Person current)
{
Person original = null;
try
{
original = await _db.People.FindAsync(current.Id);
original.FirstName = current.FirstName;
original.LastName = current.LastName;
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException e)
{
foreach (var entry in e.Entries)
{
var currentValues = entry.CurrentValues;
var databaseValues = await entry.GetDatabaseValuesAsync();
if (entry.Entity is Person person)
{
// 更新什么值取决于实际需要
person.FirstName = currentValues[nameof(Person.FirstName)]?.ToString();
person.LastName = currentValues[nameof(Person.LastName)]?.ToString();
// 这步操作是为了刷新当前 Tracker 的值, 为了通过下一次的并发检查
entry.OriginalValues.SetValues(databaseValues);
}
}
await _db.SaveChangesAsync();
}
return original;
}
这步操作也可以加入重试策略.
此时,即使在 SaveChange
的时候更新了数据库中的值(或者其他用户修改了同一实体的值),触发了并发冲突,也可以解决冲突修改为我们想要的数据。
标签:strong value rri out 提供者 帮助 tco 复制 base
原文地址:https://www.cnblogs.com/rajesh/p/11093193.html