EF Core更新含List<saleComments>的saleOrder时出现乐观并发异常
问题解答
一、乐观并发异常的原因
- 数据已被其他操作修改/删除:当你加载
SaleOrder和关联的Comments后,数据库中对应的订单或评论记录已被其他进程、用户修改或删除。EF Core的乐观并发机制会对比实体加载时的原始值与数据库当前值,若不匹配则抛出该异常。 - 评论未被EF Core正确追踪:直接替换
Comments列表(如order.Comments = newComments)会导致EF无法识别哪些评论被修改,更新时找不到对应行,触发“影响0行”的错误。 - 主键修改错误:后续出现的
PEC_SEQUENCIA错误是因为你尝试修改SaleOrder的主键属性——EF Core不允许修改实体主键,主键是实体的唯一标识,修改会破坏实体身份的一致性。
二、解决方法
1. 修复主键修改问题
- 检查所有代码,确保未对
PEC_SEQUENCIA赋值,也不要手动将其标记为修改状态(如_context.Entry(order).Property(x => x.PEC_SEQUENCIA).IsModified = true)。主键仅能在实体创建时设置,不可更新。
2. 解决乐观并发与评论更新问题
(1)配置并发令牌(RowVersion)
给SaleOrder和SaleComment添加并发令牌属性,利用数据库的timestamp(rowversion)类型自动检测并发冲突:
// 实体类定义 public class SaleOrder { public int PEC_SEQUENCIA { get; set; } // 主键 public byte[] RowVersion { get; set; } // 并发令牌 public List<SaleComment> Comments { get; set; } // 其他标量属性 } public class SaleComment { public int Id { get; set; } public int SaleOrderId { get; set; } public byte[] RowVersion { get; set; } // 可选,单独检测评论并发 // 其他属性 } // DbContext配置 protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<SaleOrder>() .Property(s => s.RowVersion) .IsRowVersion(); // 自动映射为数据库timestamp类型,作为并发令牌 modelBuilder.Entity<SaleComment>() .Property(c => c.RowVersion) .IsRowVersion(); // 可选配置 }
(2)正确追踪评论的变化
不要直接替换Comments列表,而是通过增量更新(修改现有、添加新的、删除旧的)让EF Core识别变化:
// 加载订单及关联评论 var order = _context.SaleOrders.Include(s => s.Comments) .FirstOrDefault(s => s.PEC_SEQUENCIA == orderId); // 更新订单标量属性 order.Status = updatedStatus; // 1. 更新现有评论 foreach (var existingComment in order.Comments) { var updatedComment = receivedComments.FirstOrDefault(c => c.Id == existingComment.Id); if (updatedComment != null) { existingComment.Content = updatedComment.Content; // 其他属性更新 } } // 2. 添加新评论(Id为0表示新增) foreach (var newComment in receivedComments.Where(c => c.Id == 0)) { order.Comments.Add(newComment); } // 3. 删除已移除的评论 var commentsToRemove = order.Comments .Where(c => !receivedComments.Any(uc => uc.Id == c.Id)) .ToList(); foreach (var comment in commentsToRemove) { _context.SaleComments.Remove(comment); } // 处理并发异常 try { _context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var databaseValues = entry.GetDatabaseValues(); if (databaseValues == null) { // 实体已被删除 Console.WriteLine("该订单已被其他用户删除"); } else { // 合并更改:将数据库当前值设为原始值,重新尝试保存 entry.OriginalValues.SetValues(databaseValues); _context.SaveChanges(); // 或提示用户确认合并逻辑 } }
三、高效更新关联实体并避免并发的最佳实践
- 强制使用并发令牌:通过
RowVersion实现数据库级别的乐观锁,这是最可靠的并发冲突检测方式,EF Core会自动在更新语句中加入WHERE RowVersion = 原始值的条件,确保只有未被修改的记录才会被更新。 - 增量更新关联集合:始终通过修改现有实体属性、添加/删除单个元素的方式更新
Comments,避免直接替换集合,让EF Core生成精准的SQL语句,提升性能并确保追踪正确。 - 手动追踪实体(适用于无追踪场景):如果是从前端接收未被EF追踪的实体,可通过
Attach手动标记状态,但需确保传回原始的RowVersion值以检测并发:
_context.SaleOrders.Attach(updatedOrder); _context.Entry(updatedOrder).State = EntityState.Modified; foreach (var comment in updatedOrder.Comments) { if (comment.Id == 0) { _context.SaleComments.Add(comment); } else { _context.SaleComments.Attach(comment); _context.Entry(comment).State = EntityState.Modified; } }
- 避免悲观锁:除非是极端高并发场景,否则不要使用
SELECT ... FOR UPDATE这类悲观锁,会降低数据库并发性能。
内容的提问来源于stack exchange,提问作者Otavio Salomão Ferreira
相关产品推荐
相关产品推荐

