• 《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("	{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("
    Customers 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("	{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/

  • 相关阅读:
    基于模糊Choquet积分的目标检测算法
    Android开发5:布局管理器2(表格布局TableLayout)
    JAVA WEB开发环境搭建教程
    linux下自助获取帮助
    dsp下基于双循环缓冲队列的视频采集和显示记录
    找工作笔试面试那些事儿(11)---数据库知识总结(2)范式
    【Todo】Zookeeper系列文章
    VC2010对Excel的操作
    hdu2647解题报告
    premake 在64位Ubuntu系统下编译32位GCC程序
  • 原文地址:https://www.cnblogs.com/VolcanoCloud/p/4519642.html
Copyright © 2020-2023  润新知