翻譯的初衷以及為什么選擇《Entity Framework 6 Recipes》來學習,請看本系列開篇
5-7 在別的LINQ查詢操作中使用Include()方法
問題
你有一個LINQ查詢,使用了類似這樣的操作 group by,join,和where;你想使用Include()方法預先加載額外的實體。另外你想使用Code-First來管理數據訪問。
解決方案
假設你有如圖5-22所示的概念模型
圖5-22 一個簡單的包含Club和Event以及它們之間一對多關聯的模型
在Visual Studio中添加一個名為Recipe7的控制台應用,並確保引用了實體框架6的庫,NuGet可以很好的完成這個任務。在Reference目錄上右鍵,並選擇 Manage NeGet Packages(管理NeGet包),在Online頁,定位並安裝實體框架6的包。這樣操作后,NeGet將下載,安裝和配置實體框架6的庫到你的項目中。
創建一個名為Club和Event的類,復制代碼清單5-41中的屬性到這個類中,創建實體Club和Event實體。
代碼清單5-14. Club、Event 實體類
1 public class Club 2 { 3 public Club() 4 { 5 Events = new HashSet<Event>(); 6 } 7 8 public int ClubId { get; set; } 9 public string Name { get; set; } 10 public string City { get; set; } 11 12 public virtual ICollection<Event> Events { get; set; } 13 } 14 15 public class Event 16 { 17 public int EventId { get; set; } 18 public string EventName { get; set; } 19 public DateTime EventDate { get; set; } 20 public int ClubId { get; set; } 21 22 public virtual Club Club { get; set; } 23 }
接下來,創建一個名為Recipe7Context的類,並將代碼清單5-51中的代碼添加到其中,並確保其派生到DbContext類。
1 public class Recipe7Context : DbContext 2 { 3 public Recipe7Context() 4 : base("Recipe7ConnectionString") 5 { 6 // 禁用實體框架的模型兼容性 7 Database.SetInitializer<Recipe7Context>(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="Recipe7ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
為了與group by從句組合使用Include()方法,Include()方法必須放在針對父實體的過慮和分組操作之后。如代碼清單5-17所示。
代碼清單5-17 . 當父實體上應用過慮和分組時,Include()方法的正確位置。
1 using (var context = new Recipe7Context()) 2 { 3 var club = new Club {Name = "Star City Chess Club", City = "New York"}; 4 club.Events.Add(new Event 5 { 6 EventName = "Mid Cities Tournament", 7 EventDate = DateTime.Parse("1/09/2010"), 8 Club = club 9 }); 10 club.Events.Add(new Event 11 { 12 EventName = "State Finals Tournament", 13 EventDate = DateTime.Parse("2/12/2010"), 14 Club = club 15 }); 16 club.Events.Add(new Event 17 { 18 EventName = "Winter Classic", 19 EventDate = DateTime.Parse("12/18/2009"), 20 Club = club 21 }); 22 23 context.Clubs.Add(club); 24 25 context.SaveChanges(); 26 } 27 28 using (var context = new Recipe7Context()) 29 { 30 var events = from ev in context.Events 31 where ev.Club.City == "New York" 32 group ev by ev.Club 33 into g 34 select g.FirstOrDefault(e1 => e1.EventDate == g.Min(evt => evt.EventDate)); 35 36 var eventWithClub = events.Include("Club").First(); 37 38 Console.WriteLine("The next New York club event is:"); 39 Console.WriteLine("\tEvent: {0}", eventWithClub.EventName); 40 Console.WriteLine("\tDate: {0}", eventWithClub.EventDate.ToShortDateString()); 41 Console.WriteLine("\tClub: {0}", eventWithClub.Club.Name); 42 } 43 44 Console.WriteLine("Press <enter> to continue..."); 45 Console.ReadLine();
代碼清單5-17的輸出如下:
The next New York club event is: Event: Winter Classic Date: 12/18/2009 Club: Star City Chess Club
原理
我們創建了一個俱樂部(Club)和三個Events(活動)實體對象。在查詢中,我們獲取New York 的俱樂部中的所有活動。按俱樂部分組,並查找日期中最早的活動。注意,LINQ擴展方法FirstOrDefault(),巧妙地嵌入到了Select投影操作中。然而變量events只是一個表達式,它還沒有在數據庫中執行任何操作。
接下來,我們憑借Include()方法預先加載關聯實體Club對象的信息,我們將第一個LINQ查詢變量,events,作為第二個LINQ查詢的輸入。這是LINQ組合查詢的一個示例。將一個復雜的LINQ查詢轉換成一系列的短小查詢。前面的查詢變量是后面查詢的數據源。
注意,我們使用First()方法只是為獲取第一個Event實例,這樣將返回一個Event類型,而不是Event對象的集合。實體框架6包含了一個新的名為 IQueryableExtensions的接口,它公布了Include()方法的原型,它能接受一個基於字符串或是強類型的查詢路徑作為參數。IQueryableExtensions替換了EF4和EF5中的DbExtension類。
很多開發人員覺得Include()方法很讓人迷惑,在一些情況下,智能感知不能有效地提示(因為表達式類型)。在一些情況下,它會在運行時悄悄地被忽略掉。特別地,除非編譯器無法確定其結果類型,否則不會給出警告或提示。很多問題都在運行時才會暴露出來,這會使問題更難解決。這里有一些使用Include()方法的准則:
1、Include()方法是一個在IQueryable<T>上的擴展方法;
2、Include()方法只能應用在最終的查詢結果集上,當它被在subquery(子查詢)、join(連接)或者嵌套從句中,當生成命令樹時,它將被忽略掉。在幕后,實體框架會把你的LINQ to Entites查詢轉換成一棵命令樹,然后數據庫提供者(database provider)將其處理並構建成一個用於數據庫的SQL查詢(譯注:這一點很重要,我在上面吃過虧,直到現在才弄明白)
3、Include()方法只能應用 在實體類型的結果集上,如果表達式將結果投影到一個非實體類型的類型上,Include()方法將被忽略。
4、在Include()方法和最外面的操作之間,不能改變結果集的類型。例如,一個group by從句,改變結果集的類型。
5、用於Include()方法的查詢路徑表達式必須從最外層操作返回的類型中的導航屬性開始,查詢路徑不能從任意點開始。
讓我們看看,代碼清單5-17是如何運行這些規則的,這個查詢,將活動(events)按贊助的俱樂部分組,group by將結果類型從Event改變成一個分組結果集。第四條規則告訴我們,需要在group by從句改變結果類型之后再調用Include()。我們在結尾處調用Include()方法。 如果我們過早應用Include()方法,像這樣 from ev in context.Events.Include,那么,Include()方法將被悄悄地從命令樹上移除並不在起用。
5-8 延緩加載(Deferred Loading)相關實體
問題
你有一個實體的實例,你想在一個單獨的查詢中延緩加載其中兩個或多個關聯實體。這里尤其重要的是,我們如何使用Load()方法來避免查詢相同的對象兩次。另外你想使用Code-First來管理數據訪問。
解決方案
假設你有如圖5-23所示的概念模型.
圖5-23 一個包含職員(employee),他的部門(department)和部門所在的公司(company)的模型
在Visual Studio中添加一個名為Recipe8的控制台應用,並確保引用了實體框架6的庫,NuGet可以很好的完成這個任務。在Reference目錄上右鍵,並選擇 Manage NeGet Packages(管理NeGet包),在Online頁,定位並安裝實體框架6的包。這樣操作后,NeGet將下載,安裝和配置實體框架6的庫到你的項目中。
接下來我們創建三個實體對象:Company,Departmet和Employee,復制代碼清單5-18中的屬性到這三個類中。
代碼清單5-18. 實體類
1 public class Company 2 { 3 public Company() 4 { 5 Departments = new HashSet<Department>(); 6 } 7 8 public int CompanyId { get; set; } 9 public string Name { get; set; } 10 11 public virtual ICollection<Department> Departments { get; set; } 12 } 13 14 15 public class Department 16 { 17 public Department() 18 { 19 this.Employees = new HashSet<Employee>(); 20 } 21 22 public int DepartmentId { get; set; } 23 public string Name { get; set; } 24 public int CompanyId { get; set; } 25 26 public virtual Company Company { get; set; } 27 public virtual ICollection<Employee> Employees { get; set; } 28 } 29 30 public class Employee 31 { 32 public int EmployeeId { get; set; } 33 public string Name { get; set; } 34 public int DepartmentId { get; set; } 35 36 public virtual Department Department { get; set; } 37 }
接下來,創建一個名為Recipe8Context的類,並將代碼清單5-19中的代碼添加到其中,並確保其派生到DbContext類。
1 public partial class Recipe8Context : DbContext 2 { 3 public Recipe8Context() 4 : base("Recipe8ConnectionString") 5 { 6 // 禁用實體框架的模型兼容性 7 Database.SetInitializer<Recipe8Context>(null); 8 } 9 10 protected override void OnModelCreating(DbModelBuilder modelBuilder) 11 { 12 modelBuilder.Entity<Company>().ToTable("Chapter5.Company"); 13 modelBuilder.Entity<Employee>().ToTable("Chapter5.Employee"); 14 modelBuilder.Entity<Department>().ToTable("Chapter5.Department"); 15 } 16 17 public DbSet<Company> Companies { get; set; } 18 public DbSet<Department> Departments { get; set; } 19 public DbSet<Employee> Employees { get; set; } 20 }
接下來添加App.Config文件到項目中,並使用代碼清單5-20中的代碼添加到文件的ConnectionStrings小節下。
<connectionStrings>
<add name="Recipe8ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
在圖5-23所示的模型中,一個職員(Employee)被關聯到一個確切的部門(Department)。每個部門被關聯到一個確切公司(Company)。
給定一個Employee的實例,你想加載他的部門以及部門所在的公司。是什么讓這個問題變得有點特別呢? 我們已經有了一個Employee的實例,我們想避免再一次到數據庫中獲取Emplyee對象的副本,這種情況,我們可以使用Include()方法來獲取關聯實體Company和Department。也許,在真實的情況中,Employee的獲取和實例化需要非常高的代價。
我們可以使用Load()方法,兩次去加載關聯實體,一次加載Department實例,一次去加載Company實例。然而,這會產生兩次數據庫交互。為了在一個查詢中加載關聯實體的實例,我們可以使用Include()方法和包含Department,Company查詢路徑,重新在Emlpoyee實體集上查詢。或者組合使用Reference()和Query()方法。代碼清單5-21演示了這些方法。
代碼清單5-21.將數據插入到模型並使用兩種稍有不同的方法加載關聯實體
using (var context = new Recipe8Context()) { var company = new Company {Name = "Acme Products"}; var acc = new Department {Name = "Accounting", Company = company}; var ship = new Department {Name = "Shipping", Company = company}; var emp1 = new Employee {Name = "Jill Carpenter", Department = acc}; var emp2 = new Employee {Name = "Steven Hill", Department = ship}; context.Employees.Add(emp1); context.Employees.Add(emp2); context.SaveChanges(); } // 第一種方法 using (var context = new Recipe8Context()) { // 假設我們已經擁有一個employee var jill = context.Employees.First(o => o.Name == "Jill Carpenter"); // 獲取Jill的部門和公司, 但我們需要重新加載employee var results = context.Employees.Include("Department.Company") .First(o => o.EmployeeId == jill.EmployeeId); Console.WriteLine("{0} works in {1} for {2}", jill.Name, jill.Department.Name, jill.Department.Company.Name); } //更有效的方法, 不用再加載employee using (var context = new Recipe8Context()) { // 假設我們已經擁有一個employee var jill = context.Employees.Where(o => o.Name == "Jill Carpenter").First(); //憑借Entry、Reference,Query和Include方法獲取Department和Company數據,不用去查詢底層的Employee表 context.Entry(jill).Reference(x => x.Department).Query().Include(y => y.Company).Load(); Console.WriteLine("{0} works in {1} for {2}", jill.Name, jill.Department.Name, jill.Department.Company.Name); } Console.WriteLine("Press <enter> to continue..."); Console.ReadLine();
代碼清單5-21的輸出如下:
Jill Carpenter works in Accounting for Acme Products Jill Carpenter works in Accounting for Acme Products
原理
如果我們還沒有Employee實體的實例,我們可以簡單地使用Include()方法和一個查詢路徑 Department.Company來實現 。這是我們這前使用的方法。它的缺點是,它會獲取employee實體的所有列。有很多情況下,這是一個昂貴的操作。因為我們已經加載對象到上下文中,再一次去數據庫獲取所有的列並傳輸到上下文中,這是一個浪費!
在第二個查詢中,我們使用上下文對象DbContext公布的Entry()方法訪問Employee對象並對其執行操作。然后我們鏈式調用Reference()方法和DbReferenceEntity類的Query()方法,返回一個從數據庫中加載關聯對象Deparment的查詢。另外,我們鏈式調用Include()方法來拉取關聯對象Company的信息。正如所期望的那樣,這個查詢獲取了Department和Company的數據,它沒有去獲取Employees的數據,因為這些數據已經存在於上下文對象中了。
實體框架交流QQ群: 458326058,歡迎有興趣的朋友加入一起交流
謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/