譯文,個(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): 

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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.}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

將四個(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)

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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();
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

運(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):

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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);
  }
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

第一個(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):

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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);
  }
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

運(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):

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

private static async Task<Menu> GetMenuAsync()
{  using (var context = new MenusContext())
  {
    Menu menu = await context.Menus
                      .Skip(2)
                      .FirstOrDefaultAsync();    return menu;
  }
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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):

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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();
  }
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

運(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):

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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);
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

PrepareUpdateAsync方法打開一個(gè)BookContext,并返回元組(Tuple)類型的上下文和Book對象。留意該方法被調(diào)用了兩次,并且返回與不同上下文對象相關(guān)聯(lián)的不同Book對象(代碼文件BooksSample / Program.cs):

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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);
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

注意 元組在第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):

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

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}");
  }
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

運(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

命名空間

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.ChangeTracking
System
System.Linq
System.Text
System.Threading.Tasksstatic System.Console

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

對于沖突解決,需要指定屬性,使用并發(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):

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

public class Book
{  public int BookId { get; set; }  public string Title { get; set; }  public string Publisher { get; set; } 
  public byte[] TimeStamp { get; set; }
}

Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),移動(dòng)開發(fā)培訓(xùn),云培訓(xùn)培訓(xùn)

要在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