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併發後續,好了,不早了,晚安。