EntityFramework异常:存储操作影响行数不符(0行)排查求助
异常场景
使用EntityFramework时触发异常,提示更新/插入/删除语句影响了意外的行数(0),异常发生在EditJobCommand.cs的await _context.SaveChangesAsync();代码行。已确认无其他进程同时更新该记录,不想在catch块中添加保存逻辑。
根本原因分析
结合提供的代码,核心问题集中在实体状态管理混乱和未基于数据库原始实体进行更新两个方面:
实体状态的冲突操作
代码中先调用_context.MarkModified(job)将实体状态设为EntityState.Modified,随后在OnBeforeSave方法中又将状态改为EntityState.Unchanged,再手动标记部分属性为修改状态。这种反复切换状态的操作,会导致EF生成的UPDATE语句使用当前实体的所有属性值(而非仅修改的属性)作为WHERE条件去匹配数据库记录。如果实体中存在未从前端传回的属性(比如并发令牌RowVersion、LastModified等),这些属性的默认值会与数据库实际值不匹配,导致找不到对应记录,最终更新0行。直接映射ViewModel到新实体,未加载数据库原始数据
当前代码通过_mapper.Map<DataAccess.Domain.Lab.Job>(jobViewModel)创建新实体,仅设置job.Id = command.Id后直接Attach到上下文。这种方式下,实体的所有属性值都来自ViewModel,若ViewModel未包含数据库中Job实体的全部属性(尤其是并发字段、未修改的系统字段),EF会将这些缺失属性的默认值带入UPDATE语句的WHERE条件,无法匹配数据库中的真实记录,从而触发异常。并发令牌缺失(大概率触发原因)
如果Job实体定义了并发令牌(比如带有[Timestamp]特性的RowVersion字段),但前端未将该字段的值传回,映射后的实体中该字段为默认值(如null或空字节数组)。EF在生成UPDATE语句时会将该值作为WHERE条件的一部分,与数据库中真实的RowVersion值不匹配,导致更新0行。
解决建议
1. 修复实体状态管理逻辑
删除冗余的状态切换操作,保留一种状态设置方式即可:
// 移除 _context.MarkModified(job); 这一行 // 改为Attach后直接调用OnBeforeSave设置需要修改的属性 _context.Set<DataAccess.Domain.Lab.Job>().Attach(job); OnBeforeSave(job); // 手动标记需要修改的属性
2. 改为基于数据库原始实体更新
避免直接映射ViewModel到新实体,先从数据库加载原始实体再更新属性:
var job = await _context.Set<DataAccess.Domain.Lab.Job>().FindAsync(command.Id); if (job == null) throw new KeyNotFoundException($"Job with id {command.Id} not found."); // 将ViewModel的属性映射到已加载的实体 _mapper.Map(command.JobViewModel, job); OnBeforeAttach(job); OnBeforeSave(job); // 若不需要手动标记,可移除,EF会自动跟踪变化 await _context.SaveChangesAsync();
3. 确保并发令牌(若存在)在前后端正确传递
- 检查
Job实体是否有并发令牌字段(如RowVersion),若有:- 在
JobViewModel中添加对应的字段 - 前端编辑页面需保留该字段的值并在提交时传回后端
- 映射时确保该字段的值从ViewModel正确同步到实体
- 在
4. 验证目标记录存在性
在处理更新前,先验证数据库中是否存在对应Id的记录,避免对不存在的实体执行更新操作:
var exists = await _context.Set<DataAccess.Domain.Lab.Job>().AnyAsync(j => j.Id == command.Id); if (!exists) throw new KeyNotFoundException($"Job with id {command.Id} does not exist.");
内容的提问来源于stack exchange,提问作者user19639716

