前言
之前討論過EntityFramework Core中並發問題,按照官網所給並發沖突解決方案以為沒有什么問題,但是在做單元測試時發現too young,too simple,下面我們一起來看看。
.NET Core 1.1單元測試問題
我們循序漸進,首先從單元測試開始說起,可能其中就有你在.NET Core上進行單元測試會遇到的問題,別着急,不妨一看。我們需要創建.NET Core類庫,,如下:
接下來對project.json進行如下修改。
{ "version": "1.0.0-*", "testRunner": "xunit", "dependencies": { "xunit": "2.2.0-beta2-build3300", "dotnet-test-xunit": "2.2.0-preview2-build1029" }, "frameworks": { "netcoreapp1.0": { "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" } } } } }
此時運行單元測試肯定是好使的,由於.NET Core最新為1.1版本此時我們將其版本修改為1.1如下:
"frameworks": { "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.1.0" } } } }
此時我們寫一個測試方法,如下:
public class Test { [Fact] public void PassingTest() { Assert.Equal(4, Add(2, 2)); } int Add(int x, int y) { return x + y; } }
此時運行會出現如下dotnet.exe出現異常關閉。
結果讓我們大跌眼鏡,根本不知道什么地方出錯了,此時你再運行單元測試,因為dotnet.exe已經關閉將導致單元測試無法啟動,於是乎我們通過 dotnet test 命令來運行看能否得到一點錯誤提示,結果還是讓我找到了原因。
無法加載 Microsoft.DotNet.InternalAbstractions 程序集,此時我們添加該程序集再看看,如下。
"Microsoft.DotNet.InternalAbstractions": "1.0.1-beta-003206"
完美,結果測試通過,至此關於單元測試我們有必要做下結論:官網所給對應的是針對於.net core 1.0版本,運行測試沒問題,若是.net core 1.1版本需要添加 Microsoft.DotNet.InternalAbstractions 包。我是從github上才找到解決方案【https://github.com/xunit/xunit/issues/1031】,需要添加上述依賴包才可。
EntityFramework Core 1.1並發導致顯式插入主鍵問題
我們從頭講起,在倉儲接口中定義插入Blog的接口,如下:
public interface IBlogRepository : IEntityBaseRepository<Blog> { void Create(Blog b); }
然后則是實現該接口了,如下:
public class BlogRepository : EntityBaseRepository<Blog>, IBlogRepository { private EFCoreContext _efCoreContext; public BlogRepository(EFCoreContext efCoreContext) : base(efCoreContext) { _efCoreContext = efCoreContext; } public void Create(Blog b) { try { using (var transaction = _efCoreContext.Database.BeginTransaction()) { _efCoreContext.Blogs.Add(b); _efCoreContext.SaveChanges(); transaction.Commit(); } } catch (DbUpdateConcurrencyException ex) {...} } }
接下來一切准備就緒,我們來開始進行單元測試,我們開啟兩個線程來測試看看。
[Fact] public void TestEFCore() { var blog = new Blog() { Name = "Jeffcky", Url = "http://www.cnblogs.com/CreateMyself", Posts = new List<Post>() { new Post() { Title = "a", Content = "ss" } } }; var tasks = new Task[2]; for (int i = 0; i < tasks.Length; i++) { tasks[i] = Task.Factory.StartNew(() => { var _contextOptions = new DbContextOptionsBuilder() .UseSqlServer("server=WANGPENG;Database=EFCoreDb;Trusted_Connection=True;") .Options; using (var efcoreContext = new EFCoreContext(_contextOptions)) { var blogRepository = new BlogRepository(efcoreContext); blogRepository.Create(blog); } }); } Task.WaitAll(tasks); }
此時演示結果如下,測試也通過。
當修改測試所開線程,開啟如下5個線程時。
var tasks = new Task[5];
此時將拋出異常,具體演示結果如下:
具體錯誤信息顯示如下:
當 IDENTITY_INSERT 設置為 OFF 時,不能為表 'Blog' 中的標識列插入顯式值。
當然這種情況不是一定會發生,有可能開啟兩個線程不會出現上述以上異常,有可能會拋出異常。為什么會出現上述異常呢,請看如下圖。
當一個線程過來時,正常提交肯定是沒問題,但是此時該插入的Blog已經被追蹤,僅接着又來一個線程,此時Blog中的Id是上一個線程插入的值,所以會導致我們的Id本來主鍵是自動增長的,而此時Id卻有了值出現上述異常。在項目中很難把握這樣的情況,也嘗試去修改實體的變更追蹤的狀態,結果依然出現上述問題,最終采用寫SQL語句的方式來實現,如果有能夠修改變更追蹤解決的方案請在評論中給出。在我們項目中,利用SQL語句的方式來解決EF Core的並發,同時開啟200個線程沒有出任何問題,當然我們的邏輯也還算有一點復雜,所以不用擔心EF Core的性能問題,我們更多的是關心業務邏輯。一直在思考怎么通過不寫SQL語句的方式去解決這樣的並發問題,我能夠想到的是既然傳過來的實體插入后會被變更追蹤,那么我將傳過來的參數再實例化一個對象,然后將參數傳給它這樣應該就能解決問題。
public void Create(Blog b) { var copyBlog = new Blog() { Name = b.Name, Url = b.Url }; try { using (var transaction = _efCoreContext.Database.BeginTransaction()) { _efCoreContext.Blogs.Add(copyBlog); _efCoreContext.SaveChanges(); var posts = b.Posts.Select(d => new Post() { BlogId = copyBlog.Id, Content = d.Content, Title = d.Title }); _efCoreContext.Set<Post>().AddRange(posts); _efCoreContext.SaveChanges(); transaction.Commit(); } } catch (DbUpdateConcurrencyException ex) {...} }
此時我們開啟200個線程來跑跑看看,此時測試通過,如下
我們再來看看數據庫是否已經插入200條數據。
這個對於並發導致顯式插入主鍵的問題比較另類的做法,如果有更好的方案請在評論區提出來。
總結
本節我們討論了有關EF Core中並發導致的問題,尚未找到更加可靠的方案,期待你閱讀后給出最佳方案。