SQL Server与EF Core中更新时自动递增字段的实现问题
实现EF Core基于INTEGER Version列的乐观并发控制
这个思路非常合理!基于基类统一处理Version列的并发控制,能避免重复代码,完美适配你所有表的约定。我来给你梳理具体的实现步骤和注意事项:
1. 创建带Version列的抽象基类
首先定义一个抽象基类,包含所有实体共有的Version列,并标记它为并发检查列:
public abstract class BaseEntity { // 假设所有实体都有Id主键,可根据你的实际调整 public int Id { get; set; } // 标记该列为并发检查字段 [ConcurrencyCheck] public int Version { get; set; } }
这里用[ConcurrencyCheck]而不是[Timestamp],因为[Timestamp]是专门针对SQL Server的rowversion(原timestamp)类型设计的,它是自动生成的只读字段,不适合你手动递增的场景。[ConcurrencyCheck]会告诉EF Core在更新/删除时,把这个列的值作为条件的一部分,确保只有当数据库中的值和实体中的值一致时才会执行操作。
2. 让业务实体继承基类
所有需要遵循Version约定的实体都继承这个基类,比如:
public class Product : BaseEntity { public string Name { get; set; } public decimal Price { get; set; } } public class Order : BaseEntity { public DateTime OrderDate { get; set; } public decimal TotalAmount { get; set; } }
3. 重写DbContext的SaveChanges方法统一处理Version递增
在你的DbContext中重写SaveChanges和SaveChangesAsync方法,自动为所有修改的实体递增Version值:
public class AppDbContext : DbContext { public DbSet<Product> Products { get; set; } public DbSet<Order> Orders { get; set; } // 构造函数、OnConfiguring等配置代码省略... public override int SaveChanges(bool acceptAllChangesOnSuccess) { IncrementVersionForModifiedEntities(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { IncrementVersionForModifiedEntities(); return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } private void IncrementVersionForModifiedEntities() { // 筛选出所有状态为Modified的BaseEntity派生类实体 var modifiedEntities = ChangeTracker.Entries<BaseEntity>() .Where(entry => entry.State == EntityState.Modified); foreach (var entry in modifiedEntities) { // 递增Version值 entry.Entity.Version += 1; // 手动标记Version属性为已修改,确保EF Core会将其包含在更新语句中 entry.Property(e => e.Version).IsModified = true; } } }
这段代码会在保存更改前,自动找到所有被修改的实体,把它们的Version加1,并且确保EF Core会更新这个字段。
4. 处理并发冲突
当两个请求同时更新同一行数据时,EF Core会因为Version不匹配抛出DbUpdateConcurrencyException。你需要在业务代码中捕获这个异常,处理冲突场景:
public async Task UpdateProductPrice(int productId, decimal newPrice) { try { var product = await _dbContext.Products.FindAsync(productId); if (product == null) { throw new KeyNotFoundException("产品不存在"); } product.Price = newPrice; await _dbContext.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var databaseValues = await entry.GetDatabaseValuesAsync(); if (databaseValues == null) { // 数据已被其他用户删除 Console.WriteLine("该产品已被其他用户删除,无法完成更新"); } else { var currentProduct = (Product)entry.Entity; var databaseProduct = (Product)databaseValues.ToObject(); // 这里可以根据业务需求处理,比如提示用户数据已更新,或者合并修改 Console.WriteLine($"更新失败:该产品已被其他用户修改,当前版本为{databaseProduct.Version},您的版本为{currentProduct.Version},请刷新后重试"); } } }
额外注意事项
- 新增实体的Version初始值:新增实体时Version可以默认设为0,因为新增操作不存在并发冲突(还没有其他用户能修改它)。
- 删除操作的并发检查:当删除实体时,EF Core也会检查Version列,如果数据库中的Version和实体中的不一致,同样会抛出
DbUpdateConcurrencyException,这符合乐观并发的预期,避免删除已经被修改的数据。 - 迁移配置:如果用EF Core迁移生成数据库表,它会自动将
Version列创建为INT类型,不需要额外配置。
内容的提问来源于stack exchange,提问作者Valuator
相关产品推荐
相关产品推荐

