如何处理Entity Framework / Entity Framework Core中的DbUpdateConcurrencyException异常(转载)
1. Concurrency的作用
场景
有个修改用户的页面功能,我们有一条数据User, ID是1的这个User的年龄是20, 性别是female(数据库中的原始数据)
正确的该User的年龄是25, 性别是male
这个时候A发现User的年龄不对, 就给改成25, 那么在Entity Framework中,我们会这样做。
var user = dbConext.User.Find(1); //B用户在这里完成修改了User的性别 user.age = 25; dbContext.SaveChanges();
但是假如在上面注释处,有个B用户发现性别不对,完成了对用户性别的修改,改成male. 会出现什么结果呢。
var user = dbConext.User.Find(1);
当A执行这个代码的时候,获取的性别是female
user.age = 25;
当A执行这个代码的时候, 不知道B已经修改了这个记录的性别,这个时候A的user的性别还是female
dbContext.SaveChanges();
保存修改的时候,会把female覆盖回去,这样B用户的修改就作废了。
但这不是A的本意,A其实只是想修改年龄而已。
Entity Framework使用[ConcurrencyCheck] 来解决这种问题, 当标记为[ConcurrencyCheck] 的Entity属性,如果发现在从数据库中取下来和提交的时候不一致,就会出现DbUpdateConcurrencyException异常,避免错误提交。
顺便说下,如果在实体类的属性上不用[ConcurrencyCheck]标签,在EF Core中上面这种情况是不会抛出异常的。不加[ConcurrencyCheck]标签时,EF Core中发生DbUpdateConcurrencyException异常的条件是,使用DbContext.SaveChanges方法时,生成的Update或Delete语句根据实体的Key属性值在数据库表中找不到对应行。
不过实测EF Core的实体类属性加上[ConcurrencyCheck]标签后,也会出现上面说的问题。
2. 如何正确处理DbUpdateConcurrencyException异常
2.1 放弃更新或放弃删除数据库中已经不存在的数据
原理就是,发生DbUpdateConcurrencyException异常时将实体的EntityState设置为Detached,放弃更新或放弃删除抛出DbUpdateConcurrencyException异常的实体,只更新或删除不抛出异常的实体:
using (TestDBContext dbContext = new TestDBContext()) { var jim = dbContext.Person.Where(p => p.Name == "Jim").First(); var sam = dbContext.Person.Where(p => p.Name == "Sam").First(); var jack = dbContext.Person.Where(p => p.Name == "Jack").First(); jim.Age = jim.Age + 10; sam.Age = sam.Age + 10; jack.Age = jack.Age + 10; bool saveFailed; do { saveFailed = false; try { dbContext.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { saveFailed = true; // 获取抛出DbUpdateConcurrencyException异常的实体 var entry = ex.Entries.Single(); // 设置实体的EntityState为Detached,放弃更新或放弃删除抛出异常的实体 entry.State = EntityState.Detached; } } while (saveFailed); }
也可以重写DbContext的SaveChanges方法,加入上面的逻辑来安全使用DbContext.SaveChanges方法,当Entity Framework的实体类属性上不用[ConcurrencyCheck]标签时,这种方式就很适合(也就是说,使用DbContext.SaveChanges方法时,如果生成的Update或Delete语句根据实体的Key属性值在数据库表中找不到对应行,从而抛出了DbUpdateConcurrencyException异常,这种方式就能很好地进行处理,放弃更新或放弃删除抛出异常的实体):
public override int SaveChanges() { bool saveFailed; do { saveFailed = false; try { return base.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { saveFailed = true; // 获取抛出DbUpdateConcurrencyException异常的实体 var entry = ex.Entries.Single(); // 设置实体的EntityState为Detached,放弃更新或放弃删除抛出异常的实体 entry.State = EntityState.Detached; } } while (saveFailed); return 0; }
2.2 数据库优先方式
原理是在出现异常的时候,重新加载数据库中的数据,覆盖DbContext本地数据
using (TestDBContext dbContext = new TestDBContext()) { var jim = dbContext.Person.Where(p => p.Name == "Jim").First(); var sam = dbContext.Person.Where(p => p.Name == "Sam").First(); var jack = dbContext.Person.Where(p => p.Name == "Jack").First(); jim.Age = jim.Age + 10; sam.Age = sam.Age + 10; jack.Age = jack.Age + 10; bool saveFailed; do { saveFailed = false; try { dbContext.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { saveFailed = true; // Update the values of the entity that failed to save from the store ex.Entries.Single().Reload(); } } while (saveFailed); }
2.3 客户端优先方式
以DbContext保存的客户端数据为主,覆盖数据库中的数据
using (TestDBContext dbContext = new TestDBContext()) { var jim = dbContext.Person.Where(p => p.Name == "Jim").First(); var sam = dbContext.Person.Where(p => p.Name == "Sam").First(); var jack = dbContext.Person.Where(p => p.Name == "Jack").First(); jim.Age = jim.Age + 10; sam.Age = sam.Age + 10; jack.Age = jack.Age + 10; bool saveFailed; do { saveFailed = false; try { dbContext.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { saveFailed = true; // Update original values from the database var entry = ex.Entries.Single(); entry.OriginalValues.SetValues(entry.GetDatabaseValues()); } } while (saveFailed); }
2.4 根据情况判断做相应的处理
综合前面几种方式的处理方法,根据抛出DbUpdateConcurrencyException异常实体的EntityState值做相应的处理
using (TestDBContext dbContext = new TestDBContext()) { var jim = dbContext.Person.Where(p => p.Name == "Jim").First(); var sam = dbContext.Person.Where(p => p.Name == "Sam").First(); var jack = dbContext.Person.Where(p => p.Name == "Jack").First(); jim.Age = jim.Age + 10; sam.Age = sam.Age + 10; jack.Age = jack.Age + 10; bool saveFailed; do { saveFailed = false; try { dbContext.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { saveFailed = true; var entry = ex.Entries.Single(); //The MSDN examples use Single so I think there will be only one //but if you prefer - do it for all entries //foreach(var entry in ex.Entries) //{ if (entry.State == EntityState.Deleted) //When EF deletes an item its state is set to Detached //http://msdn.microsoft.com/en-us/data/jj592676.aspx entry.State = EntityState.Detached; else entry.OriginalValues.SetValues(entry.GetDatabaseValues()); //throw; //You may prefer not to resolve when updating //} } } while (saveFailed); }
不过这种方式可能会造成死循环。。。还需要改进,原因如下:
well according to msdn.microsoft.com/en-us/library/… ex.Entries contains all the entities that could not be saved. The only problem i have with the while loop is that it will spin forever if there is an entity that could not be saved for a reason other than entry.State == EntityState.Deleted.
参考文献:
如何处理Entity Framework中的DbUpdateConcurrencyException异常
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架