如何在单个事务中避免多次调用SaveChanges?
一、关联表插入仅调用一次SaveChanges的实现
你完全不需要分两次调用SaveChangesAsync,EF Core的变更跟踪系统可以在内存中维护实体间的关联关系,一次性提交所有变更即可完成两个表的插入操作。以下是两种可行方案:
方案1:通过导航属性关联实体
如果你的实体类定义了导航属性(比如FirstModel包含ICollection<SecondModel> SecondModels,或者SecondModel包含FirstModel FirstModel),直接通过导航属性关联两个实体,EF会自动处理外键的赋值和关联:
using var transaction = await _context.Database.BeginTransaction(); try { var firstModel = new FirstModel() { // 填充FirstModel的属性 SecondModels = new List<SecondModel>() { new SecondModel() { // 填充SecondModel的属性,无需手动设置FirstModelId } } }; await _context.FirstModels.AddAsync(firstModel); // 仅调用一次SaveChanges,EF会自动插入两个实体并正确设置外键 await _context.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; // 不要吞掉异常,向上抛出以便上层处理 }
方案2:手动关联外键(无需导航属性)
如果没有定义导航属性,也可以先将两个实体都添加到上下文,通过EF的变更跟踪关联外键,同样只需一次SaveChangesAsync:
using var transaction = await _context.Database.BeginTransaction(); try { var firstModel = new FirstModel() { // 填充FirstModel的属性 }; var secondModel = new SecondModel() { // 填充SecondModel的属性 }; await _context.FirstModels.AddAsync(firstModel); await _context.SecondModels.AddAsync(secondModel); // 告诉EF两个实体的关联关系,无需等待真实Id生成 _context.Entry(secondModel).Property(nameof(SecondModel.FirstModelId)).CurrentValue = firstModel.Id; // 一次提交完成所有插入:EF会先插入FirstModel获取真实Id,再插入SecondModel并同步外键值 await _context.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; }
注:如果FirstModel.Id是数据库生成的Identity列,EF Core会在内存中先分配临时Id,SaveChangesAsync执行时会替换为数据库生成的真实值,并自动同步到关联的SecondModel外键上。
二、EF中处理类似SQL Server Inserted/Deleted表的逻辑
SQL Server的Inserted和Deleted是触发器专属的虚拟表,EF Core作为ORM并不直接暴露这些表,但可以通过以下方式实现类似需求:
1. 获取单条插入/修改后的实体数据
调用SaveChangesAsync后,上下文跟踪的实体已经更新了数据库生成的字段值(比如Id、创建时间),直接访问实体属性即可获取最新数据:
await _context.SaveChangesAsync(); // 此时firstModel.Id已经是数据库生成的真实主键值 var newFirstModelId = firstModel.Id;
2. 批量获取变更的实体
通过ChangeTracker可以获取所有待提交或已提交的变更实体:
// SaveChanges前获取待插入的实体 var pendingInserts = _context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Added) .Select(e => e.Entity) .ToList(); // SaveChanges后获取已完成变更的实体 await _context.SaveChangesAsync(); var completedChanges = _context.ChangeTracker.Entries() .Where(e => e.State is EntityState.Unchanged or EntityState.Modified) .Select(e => e.Entity) .ToList();
3. 实现类似触发器的自动业务逻辑
如果需要在实体插入/删除时自动执行逻辑(比如日志记录、关联数据更新),可以使用EF Core的SaveChangesInterceptor:
public class EntityChangeInterceptor : SaveChangesInterceptor { public override ValueTask<InterceptionResult<int>> SavingChangesAsync( DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { var context = eventData.Context; if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken); // 处理待插入的实体 var insertedEntities = context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Added) .Select(e => e.Entity); foreach (var entity in insertedEntities) { if (entity is FirstModel firstModel) { // 执行插入前/后的自定义逻辑,比如记录操作日志 } } // 同理可处理Deleted、Modified状态的实体 return base.SavingChangesAsync(eventData, result, cancellationToken); } }
然后在Program.cs中注册拦截器:
builder.Services.AddDbContext<YourDbContext>(options => { options.UseSqlServer("你的数据库连接字符串"); options.AddInterceptors(new EntityChangeInterceptor()); });
4. 直接使用SQL触发器或存储过程
如果必须依赖Inserted/Deleted表的逻辑,可以直接在SQL Server中创建触发器,或者在EF中调用包含这些表逻辑的存储过程:
// 调用存储过程 await _context.Database.ExecuteSqlRawAsync("EXEC YourTriggerLogicProc @Param", paramValue);
内容的提问来源于stack exchange,提问作者Farhad Azadaliyev

