對于并發問題這個話題相信大家并不陌生,當數據量比較大時這個時候我們就需要考慮并發,對于并發涉及到的內容也比較多,在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此時會報異常,錯誤信息如下:
See http:
因為在我們頁面上改變其值后未進行SaveChanges,但是此時我們修改了Name的值,接著再來SaveChanges,此時報上述錯誤也就是我們本節所說的并發問題。既然出現了這樣的問題,那么我們在EF Core中該如何解決出現的并發問題呢?在這里我們有兩種方式,我們一一來陳述。
既然要講并發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 row(s) but actually affected row(s). Data may have been modified or deleted since entities were loaded. See http:
接下來我們再來看看解決并發而設置行版本的情況。
當我們在插入或者更新時都會產生一個新的timestamp,這個屬性也會被當做一個并發Token來對待,它會確保當我們更新值時但是其值已經被修改過時一定會如上所述拋出異常。那么怎么使用行版本呢,(我們只講Fluent API關于Data Annotations請自行查找資料)在實體中定義如下屬性:
public byte[] RowVersion { get; set; }
接著對該屬性進行如下配置。
b.Property(p => p.RowVersion).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate();
當我們再次進行如上演示時肯定會拋出同樣的異常信息。
上述兩種從本質上都未能解決在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,我們在最后重試一次在一定程度上可以保證能夠解決并發。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。