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

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

5-2  预先加载关联实体

问题

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

解决方案

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

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Find()方法_第1张图片

图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 = "[email protected]"});
 7                 customer.CustomerEmails.Add(new CustomerEmail {Email = "[email protected]"});
 8                 context.Customers.Add(customer);
 9                 customer = new Customer {Name = "Bill Meyers", CustomerType = retail};
10                 customer.CustomerEmails.Add(new CustomerEmail {Email = "[email protected]"});
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)
[email protected]
[email protected]
Bill Meyers is a Retail Customer, email address(es)
[email protected]
Customers by Type
=================
Customer type: Web Customer
Joan Smith
[email protected]
[email protected]
Customer type: Retail Customer
Bill Meyers
[email protected]

  

原理

  默认情况下,实体框架只加载你指定的实体,这就是所谓的延迟加载。用户在你的应用中会根据他的需要浏览不同的视图,在这种情况下延迟加载很有效。

  与之相反的是,立即加载父实体和与之关联的子实体(记住,对象图是基于关联的父实体和子实体,就像数据库中基于外键的父表和子表)。它叫做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) -----第五章 加载实体和导航属性之预先加载与Find()方法_第2张图片

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

 

 

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

问题

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

解决方案

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

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Find()方法_第3张图片

图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) -----第五章 加载实体和导航属性之预先加载与Find()方法_第4张图片

 图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) -----第五章 加载实体和导航属性之预先加载与Find()方法_第5张图片

图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语句。

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Find()方法_第6张图片

图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) -----第五章 加载实体和导航属性之预先加载与Find()方法_第7张图片

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

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Find()方法_第8张图片

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

 

 

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

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

 

你可能感兴趣的:(framework)