《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Find()方法
翻译的初衷以及为什么选择《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/