譯文,個(gè)人原創(chuàng),轉(zhuǎn)載請注明出處(C# 6 與 .NET Core 1.0 高級編程 - 38 章 實(shí)體框架核心(下)),不對的地方歡迎指出與交流。
章節(jié)出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位閱讀時(shí)仔細(xì)分辨,唯望莫誤人子弟。
附英文版原文:Professional C# 6 and .NET Core 1.0 - 38 Entity Framework Core
本章節(jié)譯文分為上下篇,上篇見: C# 6 與 .NET Core 1.0 高級編程 - 38 章 實(shí)體框架核心(上)
--------------------------------------
使用對象狀態(tài)工作
創(chuàng)建數(shù)據(jù)庫后,可以進(jìn)行寫入。在第一個(gè)示例中,已添加了單個(gè)表,那么如何添加關(guān)系?
添加對象關(guān)系
以下代碼片段寫入一個(gè)關(guān)系,MenuCard包含Menu對象。MenuCard和Menu對象被實(shí)例化,然后分配雙向的關(guān)聯(lián)關(guān)系。使用Menu將 MenuCard 屬性分配給 MenuCard,而使用 MenuCard 將 Menu 屬性將填充Menu對象。 MenuCard實(shí)例被添加到調(diào)用MenuCards屬性的Add方法的上下文中。默認(rèn)情況下,向上下文添加對象時(shí)所有對象都添加樹并保存為Added 狀態(tài)。不僅保存MenuCard,還保存 Menu 對象。 設(shè)置IncludeDependents 后,所有關(guān)聯(lián)的Menu對象也將添加到上下文中。在上下文中調(diào)用SaveChanged現(xiàn)在創(chuàng)建四條記錄(代碼文件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.}
將四個(gè)對象添加到上下文后調(diào)用的方法ShowState顯示與上下文相關(guān)聯(lián)的所有對象的狀態(tài)。 DbContext類有一個(gè)ChangeTracker關(guān)聯(lián),可以使用ChangeTracker屬性訪問。 ChangeTracker的Entries方法返回變化跟蹤器的所有對象。使用foreach循環(huán),每個(gè)對象包括其狀態(tài)都將輸出到控制臺(代碼文件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(); }
運(yùn)行應(yīng)用程序以查看已Added狀態(tài)與這四個(gè)對象:
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
處于這種狀態(tài)的對象都將被SaveChangesAsync方法創(chuàng)建SQL Insert語句寫入數(shù)據(jù)庫。
對象跟蹤
可以看到上下文掌握所有被添加的對象。但上下文還需要知道所作的更改。要知道更改,檢索的每個(gè)對象都需要其在上下文中的狀態(tài)。為了看到這一點(diǎn),我們創(chuàng)建兩個(gè)返回相同對象的不同查詢。以下代碼段定義了兩個(gè)不同的查詢,其中每個(gè)查詢返回相同的對象,即存儲(chǔ)在數(shù)據(jù)庫中的Menus。實(shí)際上,只有一個(gè)對象被實(shí)現(xiàn),如同第二查詢結(jié)果一樣,檢測返回的記錄具有與已經(jīng)從上下文引用的對象相同的主鍵值。驗(yàn)證引用變量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); } }
第一個(gè)LINQ查詢返回含有比較關(guān)鍵字 LIKE 的SQL SELECT語句的結(jié)果,即以字符串“Con”開始的值:
SELECT TOP(1) [m].[MenuId], [m].[MenuCardId], [m].[Price], [m].[Text]FROM [mc].[Menus] AS [m]WHERE [m].[Text] LIKE 'Con' + '%'
第二個(gè)LINQ查詢同樣需要查詢數(shù)據(jù)庫。比較關(guān)鍵字 LIKE 以比較“(”在文本中間:
SELECT TOP(1) [m].[MenuId], [m].[MenuCardId], [m].[Price], [m].[Text]FROM [mc].[Menus] AS [m]WHERE [m].[Text] LIKE ('%' + '(') + '%'
運(yùn)行應(yīng)用程序相同的對象將寫入控制臺,并且ChangeTracker只保留一個(gè)對象。狀態(tài)是Unchanged:
the same object type: Menu, state:Unchanged, Consommé Cé lestine(with shredded pancake)
如果不需要跟蹤數(shù)據(jù)庫運(yùn)行查詢的對象,可以使用DbSet調(diào)用 AsNoTracking 方法:
var m1 = (from m in context.Menus.AsNoTracking() where m.Text.StartsWith("Con") select m).FirstOrDefault();
還可以將ChangeTracker的默認(rèn)跟蹤行為配置為QueryTrackingBehavior.NoTracking:
using (var context = new MenusContext()) { context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
使用以上的配置,數(shù)據(jù)庫進(jìn)行兩個(gè)查詢,兩個(gè)對象實(shí)現(xiàn),并且狀態(tài)信息為空。
注意 當(dāng)上下文僅用于讀取記錄且沒有更改時(shí),使用NoTracking配置非常有用。因?yàn)椴槐3譅顟B(tài)信息,可以減少上下文的開銷。
更新對象
跟蹤對象時(shí),可以輕松地更新對象,如以下代碼段所示。首先,檢索Menu對象。使用此跟蹤對象,在將更改寫入數(shù)據(jù)庫之前,會(huì)修改Price。所有更改的狀態(tài)信息將輸出到控制臺(代碼文件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); } }
運(yùn)行應(yīng)用程序可以看到對象的狀態(tài),在加載記錄后為 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
從跟蹤器訪問實(shí)體時(shí),默認(rèn)情況下會(huì)自動(dòng)檢測更改??梢酝ㄟ^設(shè)置ChangeTracker的AutoDetectChangesEnabled屬性進(jìn)行配置。要手動(dòng)查看是否已完成更改,可以調(diào)用方法DetectChanges。通過調(diào)用SaveChangesAsync,狀態(tài)將改為Unchanged。可以通過調(diào)用AcceptAllChanges方法手動(dòng)執(zhí)行此操作。
更新未跟蹤對象
對象上下文的生存周期通常是短暫的。通過ASP.NET MVC使用Entity Framework,一個(gè)HTTP請求創(chuàng)建一個(gè)對象上下文去檢索對象。從客戶端收到更新時(shí)必須再次在服務(wù)器上創(chuàng)建對象。該對象不與對象上下文相關(guān)聯(lián)。要在數(shù)據(jù)庫中更新它,該對象需要與數(shù)據(jù)上下文相關(guān)聯(lián),并且需要更改狀態(tài)去創(chuàng)建INSERT,UPDATE或DELETE語句。
下一個(gè)代碼段用來模擬這樣的場景。 GetMenuAsync方法返回一個(gè)與上下文斷開的Menu對象,在方法的結(jié)尾上下文被釋放(代碼文件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調(diào)用。該方法可以更改與任意上下文無關(guān)的Menu對象。更改后,將Menu對象傳遞給UpdateUntrackedAsync方法,將其保存在數(shù)據(jù)庫中(代碼文件MenusSample / Program.cs):
private static async Task ChangeUntrackedAsync() { Menu m = await GetMenuAsync(); m.Price += 0.7m; await UpdateUntrackedAsync(m); }
方法UpdateUntrackedAsync接收更新的對象,需要附加到上下文中。上下文附加對象的一種方法是調(diào)用DbSet的Attach方法,并根據(jù)需要設(shè)置狀態(tài)。 Update方法同時(shí)執(zhí)行一個(gè)調(diào)用:附加對象并將狀態(tài)設(shè)置為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(); } }
運(yùn)行ChangeUntrackedAsync方法的應(yīng)用程序,可以看到狀態(tài)已被更改。該對象最初未被跟蹤,但由于狀態(tài)已明確更新,所以可以看到 Modified 狀態(tài):
type: Menu, state: Modified, Cheddar Broccoli Soup
沖突處理
試想如果多個(gè)用戶同時(shí)更改相同的記錄,然后保存狀態(tài)會(huì)怎么樣?最后哪個(gè)成功保存更改?
如果訪問同一數(shù)據(jù)庫的多個(gè)用戶在不同的記錄上工作,是沒有沖突的,所有用戶都可以保存其數(shù)據(jù),也不會(huì)干擾其他用戶編輯的數(shù)據(jù)。但是,如果多個(gè)用戶在同一個(gè)記錄上工作,那么就需要考慮解決沖突的方案了。處理這個(gè)問題有很多不同的方法。最簡單的一個(gè)是,最后一個(gè)操作保存成功。最后保存數(shù)據(jù)的用戶將覆蓋先執(zhí)行更改的用戶操作。
Entity Framework還提供了選擇第一個(gè)用戶成功的方式。使用此選項(xiàng),在保存記錄時(shí)如果最初讀取的數(shù)據(jù)仍在數(shù)據(jù)庫中,則需要進(jìn)行驗(yàn)證。如果驗(yàn)證通過,讀、寫期間數(shù)據(jù)沒有更改,可以繼續(xù)保存數(shù)據(jù)。但是,如果數(shù)據(jù)更改,則需要執(zhí)行沖突解決。
讓我們進(jìn)入這些不同的選項(xiàng)。
保存最后一個(gè)操作
默認(rèn)情況是,最后一個(gè)操作保存成功。為了查看對數(shù)據(jù)庫的多個(gè)訪問,擴(kuò)展了BooksSample應(yīng)用程序。
為了容易模擬兩個(gè)用戶,方法ConflictHandlingAsync調(diào)用PrepareUpdateAsync方法兩次,對引用同一記錄的兩個(gè)Book對象進(jìn)行不同的更改,并調(diào)用UpdateAsync方法兩次。最后,圖書ID傳遞到CheckUpdateAsync方法,該方法顯示來自數(shù)據(jù)庫的圖書的實(shí)際狀態(tài)(代碼文件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方法打開一個(gè)BookContext,并返回元組(Tuple)類型的上下文和Book對象。留意該方法被調(diào)用了兩次,并且返回與不同上下文對象相關(guān)聯(lián)的不同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章“數(shù)組和元組”中進(jìn)行了解釋。
UpdateAsync方法接收了已打開的BooksContext與已更新的Book對象,將其保存到數(shù)據(jù)庫。留意這個(gè)方法同樣也被調(diào)用兩次(代碼文件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}"); } }
運(yùn)行應(yīng)用程序時(shí)會(huì)發(fā)生什么?可以看到第一次更新是成功的,第二次更新也是如此。此示例應(yīng)用程序的情況是,在更新記錄時(shí),不會(huì)驗(yàn)證在讀取記錄后是否發(fā)生任何更改。只是第二次更新覆蓋了第一次更新的數(shù)據(jù),可以看到應(yīng)用程序輸出:
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
保存第一個(gè)操作
如果需要不同的行為,例如第一個(gè)用戶的更改保存到記錄,則需要進(jìn)行一些更改。示例項(xiàng)目ConflictHandlingSample使用像之前一樣的Book和BookContext對象,但它處理first-one-wins方案。
此示例應(yīng)用程序使用以下依賴項(xiàng)和命名空間:
依賴項(xiàng)
NETStandard.Library Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.SqlServer
命名空間
Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.ChangeTracking System System.Linq System.Text System.Threading.Tasksstatic System.Console
對于沖突解決,需要指定屬性,使用并發(fā)令牌驗(yàn)證讀取和更新之間是否已發(fā)生更改?;谥付ǖ膶傩?,修改SQL UPDATE語句以不僅驗(yàn)證主鍵,還驗(yàn)證并發(fā)令牌中的所有屬性。向?qū)嶓w類型添加許多并發(fā)令牌會(huì)使用UPDATE語句創(chuàng)建一個(gè)巨大的WHERE子句,這不是很有效率。但可以在每個(gè)UPDATE語句添加一個(gè)由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屬性定義為時(shí)間戳類型,可以使用Fluent API。 SQL數(shù)據(jù)類型使用HasColumnType方法定義。每個(gè)SQL INSERT或UPDATE語句的TimeStamp屬性都會(huì)更改,方法ValueGeneratedOnAddOrUpdate通知上下文,同時(shí)在這些操作后需要使用上下文設(shè)置。 IsConcurrencyToken方法根據(jù)需要標(biāo)記此屬性,以檢查它在讀取后是否沒有更改(代碼文件ConflictHandlingSample / BooksContext.cs):
作者:沐汐 Vicky
出處:http://www.cnblogs.com/EasyInvoice
歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,否則保留追究法律責(zé)任的權(quán)利.
http://www.cnblogs.com/EasyInvoice/p/6380201.html