前言
對過年已經無感,不過還是有很多閑暇時間來學學東西和多陪陪爸媽,這一點是極好的,好了,本節我們來講講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并發後續,好了,不早了,晚安。
你所看到的并非事物本身,而是經過诠釋後所賦予的意義