翻譯的初衷以及為什么選擇《Entity Framework 6 Recipes》來學習,請看本系列開篇
5-2 預先加載關聯實體
問題
你想在一次數據交互中加載一個實體和與它相關聯實體。
解決方案
假設你有如圖5-2所示的模型。
圖5-2 包含Customer和與它相關聯信息的實體
和5-1節一樣,在模型中,有一個Customer實體,一個與它關聯的CustomerType和多個與它關聯的CustomerEamil。它與CustomerType的關系是一對多關系,這是一個實體引用(譯注:Customer中的導航屬性CustomerType)。
Customer與CustomerEmail也是一對多關系,只是這時CustomerEmail在多的這一邊。這是一個實體集合(譯注:Customer中的導航屬性CustomerEmails)。
為了在一次查詢中,獲取父對象customer和與它關聯的實體CustomerEamil和CustomrType的所有數據,我們使用Include()方法。如代清單5-2所示。
代碼清單5-2. 預先加載與Customer相關聯的CustomerType和CustomerEmail實例
1 using (var context = new EFRecipesEntities()) 2 { 3 var web = new CustomerType {Description = "Web Customer", CustomerTypeId = 1}; 4 var retail = new CustomerType {Description = "Retail Customer", CustomerTypeId = 2}; 5 var customer = new Customer {Name = "Joan Smith", CustomerType = web}; 6 customer.CustomerEmails.Add(new CustomerEmail {Email = "jsmith@gmail.com"}); 7 customer.CustomerEmails.Add(new CustomerEmail {Email = "joan@smith.com"}); 8 context.Customers.Add(customer); 9 customer = new Customer {Name = "Bill Meyers", CustomerType = retail}; 10 customer.CustomerEmails.Add(new CustomerEmail {Email = "bmeyers@gmail.com"}); 11 context.Customers.Add(customer); 12 context.SaveChanges(); 13 } 14 15 using (var context = new EFRecipesEntities()) 16 { 17 18 //Include()方法,使用基於字符串類型的,與導航屬性相對應的查詢路徑 19 var customers = context.Customers 20 .Include("CustomerType") 21 .Include("CustomerEmails"); 22 Console.WriteLine("Customers"); 23 Console.WriteLine("========="); 24 foreach (var customer in customers) 25 { 26 Console.WriteLine("{0} is a {1}, email address(es)", customer.Name, 27 customer.CustomerType.Description); 28 foreach (var email in customer.CustomerEmails) 29 { 30 Console.WriteLine("\t{0}", email.Email); 31 } 32 } 33 } 34 35 using (var context = new EFRecipesEntities()) 36 { 37 //Include()方法,使用基於強類型的,與導航屬性相對應的查詢路徑 38 var customerTypes = context.CustomerTypes 39 .Include(x => x.Customers 40 .Select(y => y.CustomerEmails)); 41 42 Console.WriteLine("\nCustomers by Type"); 43 Console.WriteLine("================="); 44 foreach (var customerType in customerTypes) 45 { 46 Console.WriteLine("Customer type: {0}", customerType.Description); 47 foreach (var customer in customerType.Customers) 48 { 49 Console.WriteLine("{0}", customer.Name); 50 foreach (var email in customer.CustomerEmails) 51 { 52 Console.WriteLine("\t{0}", email.Email); 53 } 54 } 55 } 56 }
代碼清單5-2的輸出如下:
Customers ========= Joan Smith is a Web Customer, email address(es) jsmith@gmail.com joan@smith.com Bill Meyers is a Retail Customer, email address(es) bmeyers@gmail.com Customers by Type ================= Customer type: Web Customer Joan Smith jsmith@gmail.com joan@smith.com Customer type: Retail Customer Bill Meyers bmeyers@gmail.com
原理
默認情況下,實體框架只加載你指定的實體,這就是所謂的延遲加載。用戶在你的應用中會根據他的需要瀏覽不同的視圖,在這種情況下延遲加載很有效。
與之相反的是,立即加載父實體和與之關聯的子實體(記住,對象圖是基於關聯的父實體和子實體,就像數據庫中基於外鍵的父表和子表)。它叫做Eager Loading(預先加載)。它在需要大量關聯數據時很有效,因為它在一個單獨的查詢中獲取所有的數據(父實體和與之關聯的子實體)。
在代碼清單5-2中,我們兩次使用Include()方法(譯注:第一段代碼塊中),立即獲取對象圖。第一次,我們加載一個包含Customer實體和實體引用CustmerType的對象圖。CustomerType在一對多關聯中的一這邊。第二次,我們使用Include()方法(用相同的代碼串連在一起)獲取一對多有關聯中多一邊的CustomerEmails。兩次通過fluent API方式將Include()方法鏈接在一起,我們從Customer的導航屬性獲取與其關聯的實體。注意,我們在示例中使用字符串類型來表示導航屬性,使用"."字符來分隔(譯注:示例中沒有用到,比如這樣的的形式Include(“CustomerType.Customers”))。這種字符串形式的表示方式叫做關聯實體的查詢路徑(query path)。
在接下來的代碼塊中,我們執行一樣的操作,但使用了強類型的查詢路徑。請注意我們是如何使用lambda表達式來標識每一個關聯實體的。強類型的用法給我們帶來了智能提示、編譯時檢查和重構支持。
請注意,代碼清單5-3中使用Include()方法產生的SQL查詢語句 。在結果集被實例化和返回之前,實體框架自動移除查詢中重復的數據。如圖5-3所示。
代碼清單5-3. 使用Include()方法產生的SQL查詢語句
1 SELECT 2 [Project1].[CustomerId] AS [CustomerId], 3 [Project1].[Name] AS [Name], 4 [Project1].[CustomerTypeId] AS [CustomerTypeId], 5 [Project1].[CustomerTypeId1] AS [CustomerTypeId1], 6 [Project1].[Description] AS [Description], 7 [Project1].[C1] AS [C1], 8 [Project1].[CustomerEmailId] AS [CustomerEmailId], 9 [Project1].[CustomerId1] AS [CustomerId1], 10 [Project1].[Email] AS [Email] 11 FROM ( SELECT 12 [Extent1].[CustomerId] AS [CustomerId], 13 [Extent1].[Name] AS [Name], 14 [Extent1].[CustomerTypeId] AS [CustomerTypeId], 15 [Extent2].[CustomerTypeId] AS [CustomerTypeId1], 16 [Extent2].[Description] AS [Description], 17 [Extent3].[CustomerEmailId] AS [CustomerEmailId], 18 [Extent3].[CustomerId] AS [CustomerId1], 19 [Extent3].[Email] AS [Email], 20 CASE WHEN ([Extent3].[CustomerEmailId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1] 21 FROM [Chapter5].[Customer] AS [Extent1] 22 INNER JOIN [Chapter5].[CustomerType] AS [Extent2] ON 23 [Extent1].[CustomerTypeId] = [Extent2].[CustomerTypeId] 24 LEFT OUTER JOIN [Chapter5].[CustomerEmail] AS [Extent3] ON 25 [Extent1].[CustomerId] = [Extent3].[CustomerId] 26 ) AS [Project1] 27 ORDER BY [Project1].[CustomerId] ASC, [Project1].[CustomerTypeId1] ASC, [Project1].[C1] ASC
圖5-3 通過使用Include()方法產生的冗余數據
5-3 快速查詢一個單獨的實體
問題
你想加載一個單獨的實體,但是,如果該實體已經加載到上下文中時,你不想再進行一次數據庫交互。同時,你想使用code-first 來管理數據訪問。
解決方案
假設你有如圖5-4所示的模型。
圖5-4 包含一個Club實體類型的模型
在這個模型中,我們有一個實體類型Club,你可以通過查詢獲取各種各樣的俱樂部(Clubs).
在Visual Studio中添加一個名為Recipe3的控制台應用,並確保引用了實體框架6的庫,NuGet可以很好的完成這個任務。在Reference目錄上右鍵,並選擇 Manage NeGet Packages(管理NeGet包),在Online頁,定位並安裝實體框架6的包。這樣操作后,NeGet將下載,安裝和配置實體框架6的庫到你的項目中。
創建一個名為Club類,復制代碼清單5-4中的屬性到這個類中,創建club實體。 (譯注:本書是多位作者寫的,描述的風格肯定有所不同)
代碼清單5-4. Club 實體類
1 public class Club 2 { 3 public int ClubId { get; set; } 4 public string Name { get; set; } 5 public string City { get; set; } 6 }
接下來,創建一個名為Recipe3Context的類,並將代碼清單5-5中的代碼添加到其中,並確保其派生到DbContext類。
1 public class Recipe3Context : DbContext 2 { 3 public Recipe3Context() 4 : base("Recipe3ConnectionString") 5 { 6 // 禁用實體框架的模型兼容性 7 Database.SetInitializer<Recipe3Context>(null); 8 } 9 10 protected override void OnModelCreating(DbModelBuilder modelBuilder) 11 { 12 modelBuilder.Entity<Club>().ToTable("Chapter5.Club"); 13 } 14 15 public DbSet<Club> Clubs { get; set; } 16 }
接下來添加App.Config文件到項目中,並使用代碼清單5-6中的代碼添加到文件的ConnectionStrings小節下。
<connectionStrings> <add name="Recipe3ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
如果我們正使用一個關鍵詞來搜索實體,一般是這樣操作過程,憑借Find()方法,在從數據庫中獲取之前,先在內存中查找。記住,實體框架的默認行為,當你給出一個獲取數據的操作時,它會去查詢數據庫,即使數據已經被加載到上下文中。
方法Find()是DbSet類中的成員函數,它是我們用來注冊實體到上下文對象中的類。代碼清單5-7將對此進行演示。
代碼清單5-7. 憑借實體框架中的Find()方法,避免獲取已經加載到上下文對象中的數據。
1 int starCityId; 2 int desertSunId; 3 int palmTreeId; 4 5 using (var context = new Recipe3Context()) 6 { 7 var starCity = new Club {Name = "Star City Chess Club", City = "New York"}; 8 var desertSun = new Club {Name = "Desert Sun Chess Club", City = "Phoenix"}; 9 var palmTree = new Club {Name = "Palm Tree Chess Club", City = "San Diego"}; 10 11 context.Clubs.Add(starCity); 12 context.Clubs.Add(desertSun); 13 context.Clubs.Add(palmTree); 14 context.SaveChanges(); 15 16 // SaveChanges()返回每個最新創建的Club Id 17 starCityId = starCity.ClubId; 18 desertSunId = desertSun.ClubId; 19 palmTreeId = palmTree.ClubId; 20 } 21 22 using (var context = new Recipe3Context()) 23 { 24 var starCity = context.Clubs.SingleOrDefault(x => x.ClubId == starCityId);
starCity = context.Clubs.SingleOrDefault(x => x.ClubId == starCityId); 25 starCity = context.Clubs.Find(starCityId); 26 var desertSun = context.Clubs.Find(desertSunId); 27 var palmTree = context.Clubs.AsNoTracking().SingleOrDefault(x => x.ClubId == palmTreeId); 28 palmTree = context.Clubs.Find(palmTreeId); 29 var lonesomePintId = -999; 30 context.Clubs.Add(new Club {City = "Portland", Name = "Lonesome Pine", ClubId = lonesomePintId,}); 31 var lonesomePine = context.Clubs.Find(lonesomePintId); 32 var nonexistentClub = context.Clubs.Find(10001); 33 } 34 35 Console.WriteLine("Please run this application using SQL Server Profiler..."); 36 Console.ReadLine();
原理
當使用上下文對象查詢時,即使數據已經加載到上下文中,仍會產生一次獲取數據的數據庫交互。當一次查詢完成時,不存在上下文中的實體對象將被添加到上下文中,並被跟蹤。在默認情況下,如果實體對象已經在上下文中,實體框架不會使用數據庫中較新的值重寫它。
然后, DbSet對象,它包裝着我們的實體對象,公布了一個Find()方法。特別地,Find()方法期望得到一個被查詢對象的主鍵(ID)參數。Find()方法非常有效率,因為它會先為目標對象查詢上下文。如果對象不存在,它會自動去查詢底層的數據存儲。如果仍然沒有找到,Find()方法將返回NULL給調用者。另外,Find()方法將返回已添加到上下文中(狀態為"Added"),但還沒有保存到數據庫中的對象。Find()方法對三種建模方式均有效:Database First,Model First,Code First。
在示例中,我們添加三個clubs實體到Club實體集合。請注意,在調用SaveChanges()后,我們是如何引用新創建的Club實體的ID的。當SaveChages()操作完成后,上下文會立即返回新創建對象的ID.
接下來,我們從DbContext中查詢實體,並返回StarCity Club 實體。注意,我們是如何憑借LINQ擴展方法SingleOrDefault(),返回一個對象的,如果在底層數據庫中不存在要查找的對象,它返回NULL。當發現多個符合給定條件的對象時,SingleOrDefault()方法將拋出一個異常。SingleOrDefault()在通過主鍵查找對象時,是一個非常好的方法。如果存在多個對象且你希望返回第一個時,可以考慮使用FirstOrDefault()方法。
如果你運行SQL Profiler Tool(在SQL Server Developer Edition版本或更高版本中,SQL Express版本不包含),檢查底層數據庫的活動,你會看見如圖5-5所示的SQL查詢語句產生。
圖5-5 返回 Star City Club的SQL的查詢語句
請注意圖5-5,為何在上下文對象中查詢Clubs,總是會產生一個針對底層數據庫的SQL查詢語句。這里我們獲取ID為80的Club,將數據實例化到Club實體對象,並存放在上下文對象中。有趣的是,為什么LINQ擴展方法SingleOrDefault()總是產生一個Select Top 2 的SQL查詢。 Select Top 2 這條SQL查詢確保只有一行數據被返回。 如果多於一條數據返回, 實體框架將拋出一個異常,因為 SingleOrDefault()方法保證只返回一個單獨的結果。
下一行代碼(譯注:指的是 starCity = context.Clubs.SingleOrDefault(x => x.ClubId == starCityId);),重新查詢數據庫獲取相同的對象,Star City Club。請注意,雖然對象已經存在上下文中,但實體框架DbContext的默認行為,仍會重新查詢數據庫獲取記錄。在Profiler中,我們看相同的SQL語句被產生。不僅如此,因為Star City實體已經加載到上下文中,DbContext不會使用數據庫中的新值來替換當前的值,如圖5-6所示。
圖5-6 返回Star City Club的SQL語句
下一行代碼,我們再一次查找Star City Club。然后,這次我們使用的是Find()方法,它是在DbSet類中公布的。因為Clubs是一個DbSet類,因此,我們只是在它身上簡單地調用Find()方法,並把要查找對象的主鍵作為參數傳遞線它。在我們示例中,主鍵的值為80。
Find()方法首先在上下文對象中查找Star City Club,找到對象后,它返回該對象的引用。關鍵點是,Find()方法只有在上下文中沒有找需要的對象時,才去數據庫中查詢。請注意,圖5-7中為什么沒有產生SQL語句。
圖5-7 Find()在上下文中找到了對象,沒有產生任何針對數據庫查詢語句
接下來,我們再次使用Find()方法去獲取實體對象Desert Sun Club。方法Find()沒有在上下文中找到該對象,它將查詢數據庫並返回信息。圖5-8是它查詢該對象產生的SQL語句。
圖5-8 返回Desert Sun Club對象產生的SQL語句
在下一個查詢中,我們獲取實體對象Palm Tree Club的信息,但是我們這次使用LINQ查詢。 注意AsNotracking()從句,它被添加到Clubs后面。NoTracking 選項將禁用指定對象的對象跟蹤。沒有了對象跟蹤,實體框架將不在跟蹤Palm Tree Club對象的改變。也不會將對象加載到上下文中。
隨后,當我們查詢並獲取Palm Tree Club實體對象時,Find()方法將產生一個SQL查詢語句並從數據庫從獲取實體。如圖5-9所示。因為我們使用AsNoTracking()從句指示實體框架不要在上下文中跟蹤對象,所以,數據庫交互就成了必須的了。記住,Find()方法需要對象跟蹤,以避免數據庫調用 。
圖5-9 返回Desert Sun Club實體產生的SQL查詢語句
接下來,我們添加一個新的Club實體到上下文中。我們實例化一個Club實體類,並填充必要的數據。為Id分配一個臨時的值-999。記住,我們不需要調用SaveChage()來提交新的Club對象,Lonesome Pine Club,到數據庫。有趣的是,我們使用Find()方法並給它傳遞參數-999,實體框架從上下文中返回最新創建的 Lonesome Pine Club實體對象。你可以從圖5-10中看到,這次調用Find()方法沒有產生數據庫活動。注意,Find()方法會返回一個最近添加到上下文中的實例,即使它還沒有被保存到數據庫中。
圖5-10 Find()方法在上下文中定位一個剛創建,但沒有保存的對象並返回,這個過程不生成sql查詢語句
最后,我們給Find()方法傳遞一個數據庫中不存在的Id作為參數。這個Id的值為10001.如圖5-11所示,Find()方法生成SQL查詢並試圖在數據庫中返回Id為10001的記錄。跟LINQ擴展方法SingleOrDefault()一樣,如果沒有找到指定的記錄,會向調用方返回NULL。
圖5-11 Find()方法生成一個SQL查詢,如果數據庫中不存在要查找的記錄便返回null
實體框架交流QQ群: 458326058,歡迎有興趣的朋友加入一起交流
謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/