譯文,我的原創,轉載請註明出處(C# 6 與 .NET Core 1.0 高級編程 - 38 章 實體框架核心(下)),不對的地方歡迎指出與交流。 html
章節出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位閱讀時仔細分辨,惟望莫誤人子弟。 數據庫
附英文版原文:Professional C# 6 and .NET Core 1.0 - 38 Entity Framework Core編程
本章節譯文分爲上下篇,上篇見: C# 6 與 .NET Core 1.0 高級編程 - 38 章 實體框架核心(上)數組
--------------------------------------服務器
建立數據庫後,能夠進行寫入。在第一個示例中,已添加了單個表,那麼如何添加關係?併發
添加對象關係
框架
如下代碼片斷寫入一個關係,MenuCard包含Menu對象。MenuCard和Menu對象被實例化,而後分配雙向的關聯關係。使用Menu將 MenuCard 屬性分配給 MenuCard,而使用 MenuCard 將 Menu 屬性將填充Menu對象。 MenuCard實例被添加到調用MenuCards屬性的Add方法的上下文中。默認狀況下,向上下文添加對象時全部對象都添加樹並保存爲Added 狀態。不只保存MenuCard,還保存 Menu 對象。 設置IncludeDependents 後,全部關聯的Menu對象也將添加到上下文中。在上下文中調用SaveChanged如今建立四條記錄(代碼文件MenusSample / Program.cs): async
private static async Task AddRecordsAsync() { // etc. using (var context = new MenusContext()) { var soupCard = new MenuCard(); Menu[] soups = { new Menu { Text ="Consommé Célestine (with shredded pancake)", Price = 4.8m, MenuCard = soupCard }, new Menu { Text ="Baked Potato Soup", Price = 4.8m, MenuCard = soupCard }, new Menu { Text ="Cheddar Broccoli Soup", Price = 4.8m, MenuCard = soupCard }, }; soupCard.Title ="Soups"; soupCard.Menus.AddRange(soups); context.MenuCards.Add(soupCard); ShowState(context); int records = await context.SaveChangesAsync(); WriteLine($"{records} added"); // etc. }
將四個對象添加到上下文後調用的方法ShowState顯示與上下文相關聯的全部對象的狀態。 DbContext類有一個ChangeTracker關聯,可使用ChangeTracker屬性訪問。 ChangeTracker的Entries方法返回變化跟蹤器的全部對象。使用foreach循環,每一個對象包括其狀態都將輸出到控制檯(代碼文件MenusSample/Program.cs)ide
public static void ShowState(MenusContext context) { foreach (EntityEntry entry in context.ChangeTracker.Entries()) { WriteLine($"type: {entry.Entity.GetType().Name}, state: {entry.State}," + $" {entry.Entity}"); } WriteLine(); }
運行應用程序以查看已Added狀態與這四個對象:post
type: MenuCard, state: Added, Soups type: Menu, state: Added, Consommé Célestine (with shredded pancake) type: Menu, state: Added, Baked Potato Soup type: Menu, state: Added, Cheddar Broccoli Soup
處於這種狀態的對象都將被SaveChangesAsync方法建立SQL Insert語句寫入數據庫。
能夠看到上下文掌握全部被添加的對象。但上下文還須要知道所做的更改。要知道更改,檢索的每一個對象都須要其在上下文中的狀態。爲了看到這一點,咱們建立兩個返回相同對象的不一樣查詢。如下代碼段定義了兩個不一樣的查詢,其中每一個查詢返回相同的對象,即存儲在數據庫中的Menus。實際上,只有一個對象被實現,如同第二查詢結果同樣,檢測返回的記錄具備與已經從上下文引用的對象相同的主鍵值。驗證引用變量m1和m2是否返回相同的對象(代碼文件MenusSample / Program.cs):
private static void ObjectTracking() { using (var context = new MenusContext()) { var m1 = (from m in context.Menus where m.Text.StartsWith("Con") select m).FirstOrDefault(); var m2 = (from m in context.Menus where m.Text.Contains("(") select m).FirstOrDefault(); if (object.ReferenceEquals(m1, m2)) { WriteLine("the same object"); } else { WriteLine("not the same"); } ShowState(context); } }
第一個LINQ查詢返回含有比較關鍵字 LIKE 的SQL SELECT語句的結果,即以字符串「Con」開始的值:
SELECT TOP(1) [m].[MenuId], [m].[MenuCardId], [m].[Price], [m].[Text] FROM [mc].[Menus] AS [m] WHERE [m].[Text] LIKE 'Con' + '%'
第二個LINQ查詢一樣須要查詢數據庫。比較關鍵字 LIKE 以比較「(」在文本中間:
SELECT TOP(1) [m].[MenuId], [m].[MenuCardId], [m].[Price], [m].[Text] FROM [mc].[Menus] AS [m] WHERE [m].[Text] LIKE ('%' + '(') + '%'
運行應用程序相同的對象將寫入控制檯,而且ChangeTracker只保留一個對象。狀態是Unchanged:
the same object type: Menu, state:Unchanged, Consommé Cé lestine(with shredded pancake)
若是不須要跟蹤數據庫運行查詢的對象,可使用DbSet調用 AsNoTracking 方法:
var m1 = (from m in context.Menus.AsNoTracking() where m.Text.StartsWith("Con") select m).FirstOrDefault();
還能夠將ChangeTracker的默認跟蹤行爲配置爲QueryTrackingBehavior.NoTracking:
using (var context = new MenusContext()) { context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
使用以上的配置,數據庫進行兩個查詢,兩個對象實現,而且狀態信息爲空。
注意 當上下文僅用於讀取記錄且沒有更改時,使用NoTracking配置很是有用。由於不保持狀態信息,能夠減小上下文的開銷。
跟蹤對象時,能夠輕鬆地更新對象,如如下代碼段所示。首先,檢索Menu對象。使用此跟蹤對象,在將更改寫入數據庫以前,會修改Price。全部更改的狀態信息將輸出到控制檯(代碼文件MenusSample / Program.cs):
private static async Task UpdateRecordsAsync() { using (var context = new MenusContext()) { Menu menu = await context.Menus .Skip(1) .FirstOrDefaultAsync(); ShowState(context); menu.Price += 0.2m; ShowState(context); int records = await context.SaveChangesAsync(); WriteLine($"{records} updated"); ShowState(context); } }
運行應用程序能夠看到對象的狀態,在加載記錄後爲 Unchanged,屬性值更改後爲 Modified,保存完成後爲 Unchanged:
type: Menu, state: Unchanged, Baked Potato Soup type: Menu, state: Modified, Baked Potato Soup 1 updated type: Menu, state: Unchanged, Baked Potato Soup
從跟蹤器訪問實體時,默認狀況下會自動檢測更改。能夠經過設置ChangeTracker的AutoDetectChangesEnabled屬性進行配置。要手動查看是否已完成更改,能夠調用方法DetectChanges。經過調用SaveChangesAsync,狀態將改成Unchanged。能夠經過調用AcceptAllChanges方法手動執行此操做。
對象上下文的生存週期一般是短暫的。經過ASP.NET MVC使用Entity Framework,一個HTTP請求建立一個對象上下文去檢索對象。從客戶端收到更新時必須再次在服務器上建立對象。該對象不與對象上下文相關聯。要在數據庫中更新它,該對象須要與數據上下文相關聯,而且須要更改狀態去建立INSERT,UPDATE或DELETE語句。
下一個代碼段用來模擬這樣的場景。 GetMenuAsync方法返回一個與上下文斷開的Menu對象,在方法的結尾上下文被釋放(代碼文件MenusSample / Program.cs):
private static async Task<Menu> GetMenuAsync() { using (var context = new MenusContext()) { Menu menu = await context.Menus .Skip(2) .FirstOrDefaultAsync(); return menu; } }
GetMenuAsync方法由方法ChangeUntrackedAsync調用。該方法能夠更改與任意上下文無關的Menu對象。更改後,將Menu對象傳遞給UpdateUntrackedAsync方法,將其保存在數據庫中(代碼文件MenusSample / Program.cs):
private static async Task ChangeUntrackedAsync() { Menu m = await GetMenuAsync(); m.Price += 0.7m; await UpdateUntrackedAsync(m); }
方法UpdateUntrackedAsync接收更新的對象,須要附加到上下文中。上下文附加對象的一種方法是調用DbSet的Attach方法,並根據須要設置狀態。 Update方法同時執行一個調用:附加對象並將狀態設置爲Modified(代碼文件MenusSample / Program.cs):
private static async Task UpdateUntrackedAsync(Menu m) { using (var context = new MenusContext()) { ShowState(context); // EntityEntry<Menu> entry = context.Menus.Attach(m); // entry.State = EntityState.Modified; context.Menus.Update(m); ShowState(context); await context.SaveChangesAsync(); } }
運行ChangeUntrackedAsync方法的應用程序,能夠看到狀態已被更改。該對象最初未被跟蹤,但因爲狀態已明確更新,因此能夠看到 Modified 狀態:
type: Menu, state: Modified, Cheddar Broccoli Soup
試想若是多個用戶同時更改相同的記錄,而後保存狀態會怎麼樣?最後哪一個成功保存更改?
若是訪問同一數據庫的多個用戶在不一樣的記錄上工做,是沒有衝突的,全部用戶均可以保存其數據,也不會干擾其餘用戶編輯的數據。可是,若是多個用戶在同一個記錄上工做,那麼就須要考慮解決衝突的方案了。處理這個問題有不少不一樣的方法。最簡單的一個是,最後一個操做保存成功。最後保存數據的用戶將覆蓋先執行更改的用戶操做。
Entity Framework還提供了選擇第一個用戶成功的方式。使用此選項,在保存記錄時若是最初讀取的數據仍在數據庫中,則須要進行驗證。若是驗證經過,讀、寫期間數據沒有更改,能夠繼續保存數據。可是,若是數據更改,則須要執行衝突解決。
讓咱們進入這些不一樣的選項。
默認狀況是,最後一個操做保存成功。爲了查看對數據庫的多個訪問,擴展了BooksSample應用程序。
爲了容易模擬兩個用戶,方法ConflictHandlingAsync調用PrepareUpdateAsync方法兩次,對引用同一記錄的兩個Book對象進行不一樣的更改,並調用UpdateAsync方法兩次。最後,圖書ID傳遞到CheckUpdateAsync方法,該方法顯示來自數據庫的圖書的實際狀態(代碼文件BooksSample / Program.cs):
public static async Task ConflictHandlingAsync() { // user 1 Tuple<BooksContext, Book> tuple1 = await PrepareUpdateAsync(); tuple1.Item2.Title ="updated from user 1"; // user 2 Tuple<BooksContext, Book> tuple2 = await PrepareUpdateAsync(); tuple2.Item2.Title ="updated from user 2"; // user 1 await UpdateAsync(tuple1.Item1, tuple1.Item2); // user 2 await UpdateAsync(tuple2.Item1, tuple2.Item2); context1.Item1.Dispose(); context2.Item1.Dispose(); await CheckUpdateAsync(tuple1.Item2.BookId); }
PrepareUpdateAsync方法打開一個BookContext,並返回元組(Tuple)類型的上下文和Book對象。留意該方法被調用了兩次,而且返回與不一樣上下文對象相關聯的不一樣Book對象(代碼文件BooksSample / Program.cs):
private static async Task<Tuple<BooksContext, Book>> PrepareUpdateAsync() { var context = new BooksContext(); Book book = await context.Books .Where(b => b.Title =="Conflict Handling") .FirstOrDefaultAsync(); return Tuple.Create(context, book); }
注意 元組在第7章「數組和元組」中進行了解釋。
UpdateAsync方法接收了已打開的BooksContext與已更新的Book對象,將其保存到數據庫。留意這個方法一樣也被調用兩次(代碼文件BooksSample / Program.cs):
private static async Task UpdateAsync(BooksContext context, Book book) { await context.SaveChangesAsync(); WriteLine($"successfully written to the database: id {book.BookId}" + $"with title {book.Title}"); }
CheckUpdateAsync方法將指定 id 的圖書輸出控制檯(代碼文件BooksSample / Program.cs):
private static async Task CheckUpdateAsync(int id) { using (var context = new BooksContext()) { Book book = await context.Books .Where(b => b.BookId == id) .FirstOrDefaultAsync(); WriteLine($"updated: {book.Title}"); } }
運行應用程序時會發生什麼?能夠看到第一次更新是成功的,第二次更新也是如此。此示例應用程序的狀況是,在更新記錄時,不會驗證在讀取記錄後是否發生任何更改。只是第二次更新覆蓋了第一次更新的數據,能夠看到應用程序輸出:
successfully written to the database: id 7038 with title updated from user 1
successfully written to the database: id 7038 with title updated from user 2
updated: updated from user 2
若是須要不一樣的行爲,例如第一個用戶的更改保存到記錄,則須要進行一些更改。示例項目ConflictHandlingSample使用像以前同樣的Book和BookContext對象,但它處理first-one-wins方案。
此示例應用程序使用如下依賴項和命名空間:
依賴項
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
命名空間
Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.ChangeTracking System System.Linq System.Text System.Threading.Tasks static System.Console
對於衝突解決,須要指定屬性,使用併發令牌驗證讀取和更新之間是否已發生更改。基於指定的屬性,修改SQL UPDATE語句以不只驗證主鍵,還驗證併發令牌中的全部屬性。向實體類型添加許多併發令牌會使用UPDATE語句建立一個巨大的WHERE子句,這不是頗有效率。但能夠在每一個UPDATE語句添加一個由SQL Server更新的屬性 - 這是對Book類作的。屬性TimeStamp在SQL Server中定義爲timeStamp(代碼文件ConflictHandlingSample / Book.cs):
public class Book { public int BookId { get; set; } public string Title { get; set; } public string Publisher { get; set; } public byte[] TimeStamp { get; set; } }
要在SQL Server中將TimeStamp屬性定義爲時間戳類型,可使用Fluent API。 SQL數據類型使用HasColumnType方法定義。每一個SQL INSERT或UPDATE語句的TimeStamp屬性都會更改,方法ValueGeneratedOnAddOrUpdate通知上下文,同時在這些操做後須要使用上下文設置。 IsConcurrencyToken方法根據須要標記此屬性,以檢查它在讀取後是否沒有更改(代碼文件ConflictHandlingSample / BooksContext.cs):
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); var book = modelBuilder.Entity<Book>(); book.HasKey(p => p.BookId); book.Property(p => p.Title).HasMaxLength(120).IsRequired(); book.Property(p => p.Publisher).HasMaxLength(50); book.Property(p => p.TimeStamp) .HasColumnType("timestamp") .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken(); }
注意 不只能夠在Fluent API 中使用IsConcurrencyToken方法,也能夠將屬性ConcurrencyCheck應用於要檢查併發性的屬性。
衝突處理檢查的過程相似於前面所作的。用戶1和用戶2調用PrepareUpdateAsync方法,更改書名,並調用UpdateAsync方法將更改保存到數據庫(代碼文件ConflictHandlingSample / Program.cs):
public static async Task ConflictHandlingAsync() { // user 1 Tuple<BooksContext, Book> tuple1 = await PrepareUpdateAsync(); tuple1.Item2.Title ="user 1 wins"; // user 2 Tuple<BooksContext, Book> tuple2 = await PrepareUpdateAsync(); tuple2.Item2.Title ="user 2 wins"; // user 1 await UpdateAsync(tuple1.Item1, tuple1.Item2); // user 2 await UpdateAsync(tuple2.Item1, tuple2.Item2); context1.Item1.Dispose(); context2.Item1.Dispose(); await CheckUpdateAsync(context1.Item2.BookId); }
此處不重複使用PrepareUpdateAsync方法,由於此方法以與上一個示例相同的方式實現。不一樣的是UpdateAsync方法。要查看不一樣的時間戳,在更新以前和以後,自定義擴展方法StringOutput 實現字節數組以可讀形式輸出到控制檯。接下來將顯示調用ShowChanges輔助方法對Book對象進行更改。調用SaveChangesAsync方法將全部更新寫入數據庫。若是更新失敗產生DbUpdateConcurrencyException,則會向控制檯輸出有關失敗的信息(代碼文件ConflictHandlingSample / Program.cs):
private static async Task UpdateAsync(BooksContext context, Book book, string user) { try { WriteLine($"{user}: updating id {book.BookId}," + $"timestamp: {book.TimeStamp.StringOutput()}");ShowChanges(book.BookId, context.Entry(book)); int records = await context.SaveChangesAsync(); WriteLine($"{user}: updated {book.TimeStamp.StringOutput()}"); WriteLine($"{user}: {records} record(s) updated while updating" + $"{book.Title}"); } catch (DbUpdateConcurrencyException ex) { WriteLine($"{user}: update failed with {book.Title}"); WriteLine($"error: {ex.Message}"); foreach (var entry in ex.Entries) { Book b = entry.Entity as Book; WriteLine($"{b.Title} {b.TimeStamp.StringOutput()}"); ShowChanges(book.BookId, context.Entry(book)); } } }
上下文相關聯的對象用PropertyEntry對象訪問原始值和當前值。從數據庫讀取對象時能夠用OriginalValue屬性訪問檢索的原始值,用CurrentValue屬性訪問當前值。用EntityEntry屬性方法訪問 PropertyEntry對象,以下所示ShowChanges和ShowChange方法(代碼文件ConflictHandlingSample / Program.cs):
private static void ShowChanges(int id, EntityEntry entity) { ShowChange(id, entity.Property("Title")); ShowChange(id, entity.Property("Publisher")); } private static void ShowChange(int id, PropertyEntry propertyEntry) { WriteLine($"id: {id}, current: {propertyEntry.CurrentValue}," + $"original: {propertyEntry.OriginalValue}," + $"modified: {propertyEntry.IsModified}"); }
定義擴展方法StringOutput來將從SQL Server更新的TimeStamp屬性的字節數組轉換爲可視輸出,(代碼文件ConflictHandlingSample / Program.cs):
static class ByteArrayExtension { public static string StringOutput(this byte[] data) { var sb = new StringBuilder(); foreach (byte b in data) { sb.Append($"{b}."); } return sb.ToString(); } }
運行應用程序能夠看到以下輸出。時間戳值和圖書ID每次運行都不相同。第一個用戶將標題「 sample book」的書更新爲新標題而且保存。 Title屬性的 IsModified 屬性返回true,但 Publisher屬性的 IsModified 返回false,由於只有標題已更改。原始時間戳以1.1.209結束;在更新到數據庫以後,時間戳記更改成1.17.114。同時,用戶2打開同一記錄,這本書的時間戳仍1.1.209。用戶2嘗試更新該圖書信息,但此處更新失敗,由於此圖書的時間戳與數據庫的時間戳不匹配,會拋出DbUpdateConcurrencyException異常。在異常處理程序中,異常的緣由輸出到控制檯,能夠在程序輸出中看到:
user 1: updating id 17, timestamp 0.0.0.0.0.1.1.209. id: 17, current: user 1 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False user 1: updated 0.0.0.0.0.1.17.114. user 1: 1 record(s) updated while updating user 1 wins user 2: updating id 17, timestamp 0.0.0.0.0.1.1.209. id: 17, current: user 2 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False user 2 update failed with user 2 wins user 2 error: 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. user 2 wins 0.0.0.0.0.1.1.209. id: 17, current: user 2 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False updated: user 1 wins
使用併發令牌和處理DbConcurrencyException時,能夠根據須要處理併發衝突。例如,能夠自動解決併發問題。若是更改了不一樣的屬性,能夠檢索更改的記錄併合並更改。若是更改的屬性是進行某些計算的數字(例如,點系統),則能夠從這兩個更新中增長或減小值,若是達到限制,則拋出異常。還能夠向用戶提供數據庫中當前的信息後要求用戶解決併發問題,詢問用戶想要作什麼更改。但不要問用戶詢問太多。頗有可能用戶惟一須要的是擺脫這個極少顯示的對話框,這意味着用戶可能不閱讀內容就單擊肯定或取消。對於罕見的衝突,還能夠寫入日誌並通知系統管理員須要解決問題。
第37章介紹了事務的編程。每次使用 Entity Framework 訪問數據庫都涉及事務。能夠隱式使用事務或根據須要使用配置顯式建立事務。本節中使用的示例項目以兩種方式演示事務。Menu,MenuCard和MenuContext類如前所示用於MenusSample項目。此示例應用程序使用如下依賴項和命名空間:
依賴項
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
命名空間
Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.Storage System.Linq System.Threading System.Threading.Tasks static System.Console
調用SaveChangesAsync方法會自動解析爲一個事務。若是須要完成的更改的一部分失敗,例如,因爲數據庫約束,全部已完成的更改都將回滾。經過如下代碼段演示:使用有效數據建立第一個Menu(m1)。經過提供MenuCardId來對現有MenuCard的引用完成。更新成功後,菜單m1的MenuCard屬性自動填充。可是建立第二個 Menu mInvalid 時 ,引用一個無效的 Menu Card , 並設置 MenuCardId 爲比數據庫中可用的最高ID高一個值 (譯者注:自增1) 。因爲MenuCard和Menu之間定義的外鍵關係,添加此對象將失敗(代碼文件TransactionsSample / Program.cs):
private static async Task AddTwoRecordsWithOneTxAsync() { WriteLine(nameof(AddTwoRecordsWithOneTxAsync)); try { using (var context = new MenusContext()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added", Price = 99.99m }; int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.AddRange(m1, mInvalid); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); } WriteLine(); }
調用方法AddTwoRecordsWithOneTxAsync運行應用程序後,查看數據庫的內容驗證,沒有一條記錄被添加。異常消息以及異常的內部消息給出了詳細信息:
AddTwoRecordsWithOneTxAsync An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'.
若是將第一條記錄寫入數據庫應該成功,即便第二條記錄寫入失敗,必須屢次調用SaveChangesAsync方法,以下面的代碼段所示。在方法AddTwoRecordsWithTwoTxAsync中,第一次調用SaveChangesAsync插入m1菜單對象,而第二次調用嘗試插入mInvalid菜單對象(代碼文件TransactionsSample / Program.cs):
private static async Task AddTwoRecordsWithTwoTxAsync() { WriteLine(nameof(AddTwoRecordsWithTwoTxAsync)); try { using (var context = new MenusContext()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added", Price = 99.99m }; context.Menus.Add(m1); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.Add(mInvalid); records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); } WriteLine(); }
運行應用程序時,第一個INSERT語句添加成功,固然第二個會致使DbUpdateException。能夠查看數據庫驗證,這次添加了一條記錄:
AddTwoRecordsWithTwoTxAsync 1 records added An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'.
除了隱式建立事務,也能夠顯式地建立它們。這提供了一個優勢,便可以選擇回滾,以防某些業務邏輯失敗,而且能夠在一個事務中合併多個SaveChangesAsync調用。要啓動DbContext派生類相關聯的事務,須要調用從Database屬性返回的DatabaseFacade類的BeginTransactionAsync方法。事務返回接口IDbContextTransactio的實現。用關聯的DbContext完成的SQL語句加入到事務中。要提交或回滾,必須顯式調用方法Commit或Rollback。示例代碼中,在達到DbContext做用域結束時執行Commit,發生異常則回滾(代碼文件TransactionsSample / Program.cs)的狀況下完成:
private static async Task TwoSaveChangesWithOneTxAsync() { WriteLine(nameof(TwoSaveChangesWithOneTxAsync)); IDbContextTransaction tx = null; try { using (var context = new MenusContext()) using (tx = await context.Database.BeginTransactionAsync()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added with explicit tx", Price = 99.99m }; context.Menus.Add(m1); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.Add(mInvalid); records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); tx.Commit(); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); WriteLine("rolling back…"); tx.Rollback(); } WriteLine(); }
運行應用程序能夠看到沒有添加任何記錄,但SaveChangesAsync方法被屢次調用。第一次返回SaveChangesAsync時,會將一條記錄列爲已添加的記錄,但此記錄基於Rollback稍後被移除。根據設置的隔離級別,更新的記錄只能在事務內完成回滾以前查看,不能在事務外部查看。
TwoSaveChangesWithOneTxAsync 1 records added An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'. rolling back…
注意經過BeginTransactionAsync方法,還能夠提供隔離級別的值去指定數據庫中所需的隔離要求和鎖定。隔離級別在第37章中作了討論。
本章介紹了Entity Framework Core的功能。瞭解對象上下文如何保存有關檢索和更新的實體的狀況,以及如何將更改寫入數據庫。瞭解如何使用遷移用C#代碼建立和更改數據庫結構。瞭解如何使用數據批註來完成數據庫映射去定義結構,還看到了與批註相比提供更多功能的Fluent API。
多個用戶在同一個記錄上工做時對衝突作出反應的可能性,隱式或顯式地使用事務進行事務控制。
下一章將展現利用Windows Services 建立一個系統自動啓動的程序,能夠在Windows服務中使用Entity Framework。
(本章完)