转载

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

翻译的初衷以及为什么选择《Entity Framework 6 Recipes》来学习,请看本系列开篇

5-2  预先加载关联实体

问题

你想在一次数据交互中加载一个实体和与它相关联实体。

解决方案

假设你有如图5-2所示的模型。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图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

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图5-3 通过使用Include()方法产生的冗余数据

5-3  快速查询一个单独的实体

问题

你想加载一个单独的实体,但是,如果该实体已经加载到上下文中时,你不想再进行一次数据库交互。同时,你想使用code-first 来管理数据访问。

解决方案

假设你有如图5-4所示的模型。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图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查询语句产生。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图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所示。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图5-6 返回Star City Club的SQL语句

下一行代码,我们再一次查找Star City Club。然后,这次我们使用的是Find()方法,它是在DbSet类中公布的。因为Clubs是一个DbSet类,因此,我们只是在它身上简单地调用Find()方法,并把要查找对象的主键作为参数传递线它。在我们示例中,主键的值为80。

Find()方法首先在上下文对象中查找Star City Club,找到对象后,它返回该对象的引用。关键点是,Find()方法只有在上下文中没有找需要的对象时,才去数据库中查询。请注意,图5-7中为什么没有产生SQL语句。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图5-7 Find()在上下文中找到了对象,没有产生任何针对数据库查询语句

接下来,我们再次使用Find()方法去获取实体对象Desert Sun Club。方法Find()没有在上下文中找到该对象,它将查询数据库并返回信息。图5-8是它查询该对象产生的SQL语句。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图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()方法需要对象跟踪,以避免数据库调用 。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图5-9 返回Desert Sun Club实体产生的SQL查询语句

接下来,我们添加一个新的Club实体到上下文中。我们实例化一个Club实体类,并填充必要的数据。为Id分配一个临时的值-999。记住,我们不需要调用SaveChage()来提交新的Club对象,Lonesome Pine Club,到数据库。有趣的是,我们使用Find()方法并给它传递参数-999,实体框架从上下文中返回最新创建的 Lonesome Pine Club实体对象。你可以从图5-10中看到,这次调用Find()方法没有产生数据库活动。注意,Find()方法会返回一个最近添加到上下文中的实例,即使它还没有被保存到数据库中。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图5-10 Find()方法在上下文中定位一个刚创建,但没有保存的对象并返回,这个过程不生成sql查询语句

最后,我们给Find()方法传递一个数据库中不存在的Id作为参数。这个Id的值为10001.如图5-11所示,Find()方法生成SQL查询并试图在数据库中返回Id为10001的记录。跟LINQ扩展方法SingleOrDefault()一样,如果没有找到指定的记录,会向调用方返回NULL。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Fin...

图5-11 Find()方法生成一个SQL查询,如果数据库中不存在要查找的记录便返回null

实体框架交流QQ群:  458326058,欢迎有兴趣的朋友加入一起交流

谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/

正文到此结束
Loading...