前言
對過年已經無感,不過還是有很多閑暇時間來學學東西和多陪陪爸媽,這一點是極好的,好了,本節我們來講講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並發后續,好了,不早了,晚安。