譯文,個人原創,轉載請注明出處(C# 6 與 .NET Core 1.0 高級編程 - 38 章 實體框架核心(下)),不對的地方歡迎指出與交流。
章節出自《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):
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)
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狀態與這四個對象:
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。
(本章完)