You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core更新含List<saleComments>的saleOrder时出现乐观并发异常

问题解答

一、乐观并发异常的原因

  1. 数据已被其他操作修改/删除:当你加载SaleOrder和关联的Comments后,数据库中对应的订单或评论记录已被其他进程、用户修改或删除。EF Core的乐观并发机制会对比实体加载时的原始值与数据库当前值,若不匹配则抛出该异常。
  2. 评论未被EF Core正确追踪:直接替换Comments列表(如order.Comments = newComments)会导致EF无法识别哪些评论被修改,更新时找不到对应行,触发“影响0行”的错误。
  3. 主键修改错误:后续出现的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();
        // 或提示用户确认合并逻辑
    }
}

三、高效更新关联实体并避免并发的最佳实践

  1. 强制使用并发令牌:通过RowVersion实现数据库级别的乐观锁,这是最可靠的并发冲突检测方式,EF Core会自动在更新语句中加入WHERE RowVersion = 原始值的条件,确保只有未被修改的记录才会被更新。
  2. 增量更新关联集合:始终通过修改现有实体属性、添加/删除单个元素的方式更新Comments,避免直接替换集合,让EF Core生成精准的SQL语句,提升性能并确保追踪正确。
  3. 手动追踪实体(适用于无追踪场景):如果是从前端接收未被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;
    }
}
  1. 避免悲观锁:除非是极端高并发场景,否则不要使用SELECT ... FOR UPDATE这类悲观锁,会降低数据库并发性能。

内容的提问来源于stack exchange,提问作者Otavio Salomão Ferreira

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 21:51:21