EntityFramework Core解决并发详解
前言
对过年已经无感,不过还是有很多闲暇时间来学学东西和多陪陪爸妈,这一点是极好的,好了,本节我们来讲讲EntityFramework Core中的并发问题。
话题(EntityFramework Core并发)
对于并发问题这个话题相信大家并不陌生,当数据量比较大时这个时候我们就需要考虑并发,对于并发涉及到的内容也比较多,在EF Core中我们将并发分为几个小节来陈述,让大家看起来也不太累,也容易接受,我们由浅入深。首先我们看下给出的Blog实体类。
public class Blog : IEntityBase { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public ICollection<Post> Posts { get; set; } }
对于在VS2015中依赖注入仓储我们就不再叙述,比较简单,我们看下控制器中的两个方法,一个是渲染数据,一个是更新数据的方法,如下:
public class HomeController : Controller { private IBlogRepository _blogRepository; public HomeController(IBlogRepository blogRepository) { _blogRepository = blogRepository; } public IActionResult Index() { var blog = _blogRepository.GetSingle(d => d.Id == 1); return View(blog); } [HttpPost] public IActionResult Index(Blog obj) { try { _blogRepository.Update(obj); _blogRepository.Commit(); } catch (Exception ex) { ModelState.AddModelError("", ex.Message); } return View(obj); } }
视图渲染数据如下:
@using StudyEFCore.Model.Entities @model Blog <html> <head> <title></title> </head> <body> @using (Html.BeginForm("Index", "Home", FormMethod.Post)) { <table border="1" cellpadding="10"> <tr> <td>博客ID :</td> <td> @Html.TextBoxFor(m => m.Id, new { @readonly = "readonly" }) </td> </tr> <tr> <td>博客名称 :</td> <td>@Html.TextBoxFor(m => m.Name)</td> </tr> <tr> <td>博客地址:</td> <td>@Html.TextBoxFor(m => m.Url)</td> </tr> <tr> <td colspan="2"> <input type="submit" value="更新" /> </td> </tr> </table> } @Html.ValidationSummary() </body> </html>
最终在页面上渲染的数据如下:
接下来我们演示下如何引起并发问题,如下:
上述我们通过在视图页面更新值后然后在SaveChanges之前打断点,然后我们在数据库中改变其值,再来SaveChanges此时会报异常,错误信息如下:
Database operation expected to affect 1 row(s) but actually affected 0 row(s).
Data may have been modified or deleted since entities were loaded.
See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
因为在我们页面上改变其值后未进行SaveChanges,但是此时我们修改了Name的值,接着再来SaveChanges,此时报上述错误也就是我们本节所说的并发问题。既然出现了这样的问题,那么我们在EF Core中该如何解决出现的并发问题呢?在这里我们有两种方式,我们一一来陈述。
EF Core并发解决方案一(并发Token)
既然要讲并发Token,那么在此之前我们需要讲讲并发Token到底是怎样工作的,当我们对属性标识为并发Token,当我们从数据库中加载其值时,此时对应的属性的并发Token也就通过上下文而分配,当对分配的并发Token属性的相同的值进行了更新或者删除,此时会强制该属性的并发Token去进行检测,它会去检测影响的行数量,如果并发已经匹配到了,然后一行将被更新到,如果该值在数据库中已经被更新,那么将没有数据行会被更新。对于更新或者删除通过在WHERE条件上包括并发Token。接下来我们对要更新的Name将其设置为并发Token,如下:
public class BlogMap : EntityMappingConfiguration<Blog> { public override void Map(EntityTypeBuilder<Blog> b) { b.ToTable("Blog"); b.HasKey(k => k.Id); b.Property(p => p.Name).IsConcurrencyToken(); b.Property(p => p.Url); b.HasMany(p => p.Posts).WithOne(p => p.Blog).HasForeignKey(p => p.BlogId); } }
当我们进行如上设置后再来迁移更新模型,最终还是会抛出如下异常:
Database operation expected to affect 1 row(s) but actually affected 0 row(s).
Data may have been modified or deleted since entities were loaded.
See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
接下来我们再来看看解决并发而设置行版本的情况。
EF Core并发解决方案二(行版本)
当我们在插入或者更新时都会产生一个新的timestamp,这个属性也会被当做一个并发Token来对待,它会确保当我们更新值时但是其值已经被修改过时一定会如上所述抛出异常。那么怎么使用行版本呢,(我们只讲Fluent API关于Data Annotations请自行查找资料)在实体中定义如下属性:
public byte[] RowVersion { get; set; }
接着对该属性进行如下配置。
b.Property(p => p.RowVersion).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate();
当我们再次进行如上演示时肯定会抛出同样的异常信息。
上述两种从本质上都未能解决在EF Core中的并发问题只是做了基础的铺垫,那么我们到底该如何做才能解决并发问题呢,请继续往下看。
解析EF Core并发冲突
我们通过三种设置来解析EF Core中的并发冲突,如下:
当前值(Current values):试图将当前修改的值写入到到数据库。
原始值(Original values):在未做任何修改时的需要从数据库中检索到的值。
数据值(Database values):当前保存在数据库中的值。
由于并发会抛出异常,所以我们需要 在SaveChanges时在并发冲突所产生的异常中来进行解决,并发异常呈现在 DbUpdateConcurrencyException 类中,我们只需要在此并发异常类解决即可。比如上述我们需要修改Name的值,我们做了基础的铺垫,设置了并发Token。但是还是会引发并发异常,未能解决问题,这个只是解决并发异常的前提,由于我们利用的仓储来操作数据,但是并发异常会利用到EF上下文,所以我们额外定义接口,直接通过上下文来操作,如下我们定义一个接口
public interface IBlogRepository : IEntityBaseRepository<Blog> { void UpdateBlog(Blog blog); }
解决并发异常通过EF上下文来操作。
public class BlogRepository : EntityBaseRepository<Blog>, IBlogRepository { private EFCoreContext _efCoreContext; public BlogRepository(EFCoreContext efCoreContext) : base(efCoreContext) { _efCoreContext = efCoreContext; } public void UpdateBlog(Blog blog) { try { _efCoreContext.Set<Blog>().Update(blog); _efCoreContext.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { foreach (var entry in ex.Entries) { if (entry.Entity is Blog) { var databaseEntity = _efCoreContext.Set<Blog>().AsNoTracking().Single(p => p.Id == ((Blog)entry.Entity).Id); var databaseEntry = _efCoreContext.Entry(databaseEntity); foreach (var property in entry.Metadata.GetProperties()) { var proposedValue = entry.Property(property.Name).CurrentValue; var originalValue = entry.Property(property.Name).OriginalValue; var databaseValue = databaseEntry.Property(property.Name).CurrentValue; // TODO: Logic to decide which value should be written to database var propertyName = property.Name; if (propertyName == "Name") { // Update original values to entry.Property(property.Name).OriginalValue = databaseEntry.Property(property.Name).CurrentValue; break; } } } else { throw new NotSupportedException("Don't know how to handle concurrency conflicts for " + entry.Metadata.Name); } } // Retry the save operation _efCoreContext.SaveChanges(); } } }
上述则是通用解决并发异常的办法,我们只是注意上述表明的TODO逻辑,我们需要得到并发的属性,然后再来更新其值即可,我们对于Name会产生并发,所以遍历实体属性时获取到Name,然后更新其值即可,简单粗暴,完胜。我们看如下演示。
上述我们将Name修改为efcoreefcore,在SaveChanges前修改数据库中的Name,接着再来进行SaveChanges时,此时肯定会走并发异常,我们在并发异常中进行处理,最终我们能够很清楚的看到最终数据库中的Name更新为efcoreefcore,我们在最后重试一次在一定程度上可以保证能够解决并发。
总结
本节我们比较详细的讲解了EntityFramework Core中的并发问题以及该如何解决,到这里算是基本结束,我才发现在项目当中未经测试我居然用错了,明天去修改修改,这里算是一个稍微详细的讲解吧,如果进行压力测试不知道结果会怎样,后续进行压力测试若有进一步的进展再来完善,到时再来更新EF Core并发后续,好了,不早了,晚安。