《Entity Framework 6 Recipes》中文翻译系列 (28) ------ 第五章 加载实体和导航属性之测试实体是否加载与显式加载关联实体

时间:2023-03-08 17:30:10

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

5-11  测试实体引用或实体集合是否加载

问题

  你想测试关联实体或实体集合是否已经加载到上下文中,另外你想使用Code-First来管理数据访问。

解决方案

  假设你有如图5-26所示的概念模型《Entity Framework 6 Recipes》中文翻译系列 (28) ------ 第五章 加载实体和导航属性之测试实体是否加载与显式加载关联实体

图5-26 一个包含projects,managers和contractors的模型

在Visual Studio中添加一个名为Recipe11的控制台应用,并确保引用了实体框架6的库,NuGet可以很好的完成这个任务。在Reference目录上右键,并选择 Manage NeGet Packages(管理NeGet包),在Online页,定位并安装实体框架6的包。这样操作后,NeGet将下载,安装和配置实体框架6的库到你的项目中。

  接下来我们创建三个实体对象:Contractor,Manager,Project,复制代码清单5-27中的属性到这三个类中。

代码清单5-27. 实体类

 public class Contractor
{
public int ContracterID { get; set; }
public string Name { get; set; }
public int ProjectID { get; set; } public virtual Project Project { get; set; }
} public class Manager
{
public Manager()
{
Projects = new HashSet<Project>();
} public int ManagerID { get; set; }
public string Name { get; set; } public virtual ICollection<Project> Projects { get; set; }
} public class Project
{
public Project()
{
Contractors = new HashSet<Contractor>();
} public int ProjectID { get; set; }
public string Name { get; set; }
public int ManagerID { get; set; } public virtual ICollection<Contractor> Contractors { get; set; }
public virtual Manager Manager { get; set; }
}

接下来,创建一个名为Recipe11Context的类,并将代码清单5-24中的代码添加到其中,并确保其派生到DbContext类。

代码清单5-28. 上下文

  public class Recipe11Context : DbContext
{
public Recipe11Context()
: base("Recipe11ConnectionString")
{
//禁用实体框架的模型兼容
Database.SetInitializer<Recipe11Context>(null);
} public DbSet<Contractor> Contractors { get; set; }
public DbSet<Manager> Managers { get; set; }
public DbSet<Project> Projects { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Contractor>().ToTable("Chapter5.Contractor");
modelBuilder.Entity<Manager>().ToTable("Chapter5.Manager");
modelBuilder.Entity<Project>().ToTable("Chapter5.Project"); // 显示映射实体键
modelBuilder.Entity<Contractor>().HasKey(x => x.ContracterID);
}
}

接下来添加App.Config文件到项目中,并使用代码清单5-25中的代码添加到文件的ConnectionStrings小节下。

代码清单5-29. 连接字符串

《Entity Framework 6 Recipes》中文翻译系列 (28) ------ 第五章 加载实体和导航属性之测试实体是否加载与显式加载关联实体
<connectionStrings>
<add name="Recipe11ConnectionString"
connectionString="Data Source=.;
Initial Catalog=EFRecipes;
Integrated Security=True;
MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
《Entity Framework 6 Recipes》中文翻译系列 (28) ------ 第五章 加载实体和导航属性之测试实体是否加载与显式加载关联实体

  实体框架公布了一个IsLoaded属性。只要它100%的确定,指定的实体或实体集合全部已经加载且在上下文中有效时,它的值便为True。图5-26中的模型表示,项目(Project),项目的管理者(manager)以及项目的承包商(Contractors)。按代码清单5-30所示,测试关联实体是否加载到上下文对象中。

代码清单5-30.使用IsLoaded判断一个实体或实体集合是否已加载到上下文中

using (var context = new Recipe11Context())
{
var man1 = new Manager {Name = "Jill Stevens"};
var proj = new Project {Name = "City Riverfront Park", Manager = man1};
var con1 = new Contractor {Name = "Robert Alvert", Project = proj};
var con2 = new Contractor {Name = "Alan Jones", Project = proj};
var con3 = new Contractor {Name = "Nancy Roberts", Project = proj};
context.Projects.Add(proj);
context.SaveChanges();
} using (var context = new Recipe11Context())
{
var project = context.Projects.Include("Manager").First(); if (context.Entry(project).Reference(x => x.Manager).IsLoaded)
Console.WriteLine("Manager entity is loaded.");
else
Console.WriteLine("Manager entity is NOT loaded."); if (context.Entry(project).Collection(x => x.Contractors).IsLoaded)
Console.WriteLine("Contractors are loaded.");
else
Console.WriteLine("Contractors are NOT loaded.");
Console.WriteLine("Calling project.Contractors.Load()..."); context.Entry(project).Collection(x => x.Contractors).Load(); if (context.Entry(project).Collection(x => x.Contractors).IsLoaded)
Console.WriteLine("Contractors are now loaded.");
else
Console.WriteLine("Contractors failed to load.");
} Console.WriteLine("Press <enter> to continue...");
Console.ReadLine();

代码清单5-30的输出如下:

Manager entity is loaded.
Contractors are NOT loaded.
Calling project.Contractors.Load()...
Contractors are now loaded.

原理

  我们使用Include()方法,从数据库中预先加载Project实体和与它关联的Manager。

  查询之后,我们通过reference()方法获取关联实体Manager的引用和检查IsLoaded属性的值,来判断manager实例是否加载。因为这是一个实体引用(引用一个单独的父实体),调用Entry()方法返回DbEntityEntry类型上的Reference方法,它返回类型上IsLoaded属性有效。因为我已经加载了Projects和Manager,所以,该属性返回True.

  接下来我们检查Contractor实体集合是否加载。因为我们没有在Include()方法中预先加载它,也没有使用Load()方法直接加载过它,所以它没有被加载。只要一加载它。IsLoaded属性便会被设置为True.

  默认行为,延迟加载,开启时,当关联实体或实体集合被引用时,IsLoaded属性便会被设置为True。延迟加载会让实体框架在实体或实体集合被引用时,自动加载。显式加载和延迟加载有些类似,只是它不是自动的。开发人员必须显式地使用Load()方法来加载实体,它让开发人员可以完全自己控制是否加载,何时加载关联实体。

  IsLoaded确切的含义,比看起来更让人迷惑。IsLoaded被调用Load()方法的查询设置,也被隐式的关系跨度设置。当你查询一个实体时,会隐式查询关联实体Key。如果这个隐式查询的结果是一个null值,IsLoaded会被设置成True,意思是数据库没有关联的实体。当我们显示加载关系并发现没有关联实体时,IsLoaded同样会被设置成True. (译注:这里可能会有点难理解,因为涉及到了一个术语:关系跨度(Relationship Span)的理解,它是指EF加载实体时总是一起返回外键值,以此来避免一些列的问题)

5-12  显示加载关联实体

问题

  你想直接加载关联实体,不依赖默认的延迟加载功能。

解决方案

  假设你有如图5-27所示的概念模型

《Entity Framework 6 Recipes》中文翻译系列 (28) ------ 第五章 加载实体和导航属性之测试实体是否加载与显式加载关联实体

图5-27 一个包含实体 doctor、appointment、patient的模型

  图5-27描述的模型,表示医生(doctor)和他的患者(patient),以及预约(appointment)。代码清单5-31,显示加载关联实体。

代码清单5-31. 使用Load()方法

  using (var context = new EFRecipesEntities())
{
var doc1 = new Doctor { Name = "Joan Meyers" };
var doc2 = new Doctor { Name = "Steven Mills" };
var pat1 = new Patient { Name = "Bill Rivers" };
var pat2 = new Patient { Name = "Susan Stevenson" };
var pat3 = new Patient { Name = "Roland Marcy" }; var app1 = new Appointment
{
Date = DateTime.Today,
Doctor = doc1,
Fee = 109.92M,
Patient = pat1,
Reason = "Checkup"
};
var app2 = new Appointment
{
Date = DateTime.Today,
Doctor = doc2,
Fee = 129.87M,
Patient = pat2,
Reason = "Arm Pain"
};
var app3 = new Appointment
{
Date = DateTime.Today,
Doctor = doc1,
Fee = 99.23M,
Patient = pat3,
Reason = "Back Pain"
}; context.Appointments.Add(app1);
context.Appointments.Add(app2);
context.Appointments.Add(app3); context.SaveChanges();
} using (var context = new EFRecipesEntities())
{
// 禁用延迟加载,因为我们要显式加载子实体
context.Configuration.LazyLoadingEnabled = false; var doctorJoan = context.Doctors.First(o => o.Name == "Joan Meyers"); if (!context.Entry(doctorJoan).Collection(x => x.Appointments).IsLoaded)
{
context.Entry(doctorJoan).Collection(x => x.Appointments).Load();
Console.WriteLine("Dr. {0}'s appointments were explicitly loaded.",
doctorJoan.Name);
} Console.WriteLine("Dr. {0} has {1} appointment(s).",
doctorJoan.Name,
doctorJoan.Appointments.Count()); foreach (var appointment in context.Appointments)
{
if (!context.Entry(appointment).Reference(x => x.Doctor).IsLoaded)
{
context.Entry(appointment).Reference(x => x.Doctor).Load();
Console.WriteLine("Dr. {0} was explicitly loaded.",
appointment.Doctor.Name);
}
else
Console.WriteLine("Dr. {0} was already loaded.",
appointment.Doctor.Name);
} Console.WriteLine("There are {0} appointments for Dr. {1}",
doctorJoan.Appointments.Count(),
doctorJoan.Name); doctorJoan.Appointments.Clear(); Console.WriteLine("Collection clear()'ed");
Console.WriteLine("There are now {0} appointments for Dr. {1}",
doctorJoan.Appointments.Count(),
doctorJoan.Name); context.Entry(doctorJoan).Collection(x => x.Appointments).Load(); Console.WriteLine("Collection loaded()'ed"); Console.WriteLine("There are now {0} appointments for Dr. {1}",
doctorJoan.Appointments.Count().ToString(),
doctorJoan.Name); //目前,DbContext 没有API去刷新实体,但底层的ObjectContext有,执行下面的动作。
var objectContext = ((IObjectContextAdapter)context).ObjectContext;
var objectSet = objectContext.CreateObjectSet<Appointment>();
objectSet.MergeOption = MergeOption.OverwriteChanges;
objectSet.Load(); Console.WriteLine("Collection loaded()'ed with MergeOption.OverwriteChanges"); Console.WriteLine("There are now {0} appointments for Dr. {1}",
doctorJoan.Appointments.Count(),
doctorJoan.Name);
} //演示先加载部分实体集合,然后再加载剩下的
using (var context = new EFRecipesEntities())
{
// 禁用延迟加载,因为我们要显式加载子实体
context.Configuration.LazyLoadingEnabled = false; //加载第一个doctor然后只附加一个appointment
var doctorJoan = context.Doctors.First(o => o.Name == "Joan Meyers");
context.Entry(doctorJoan).Collection(x => x.Appointments).Query().Take().Load(); //注意,这里IsLoaded返回False,因为所有的实体还没有被加载到上下文
var appointmentsLoaded = context.Entry(doctorJoan).Collection(x => x.Appointments).IsLoaded; Console.WriteLine("Dr. {0} has {1} appointments loaded.",
doctorJoan.Name,
doctorJoan.Appointments.Count()); //当我需要加载剩下的appointments,只需要简单的调用Load()来加载它们
context.Entry(doctorJoan).Collection(x => x.Appointments).Load();
Console.WriteLine("Dr. {0} has {1} appointments loaded.",
doctorJoan.Name,
doctorJoan.Appointments.Count());
} Console.WriteLine("Press <enter> to continue...");
Console.ReadLine();

代码清单5-31的输出如下:

Dr. Joan Meyers's appointments were explicitly loaded.
Dr. Joan Meyers has appointment(s).
Dr. Joan Meyers was already loaded.
Dr. Steven Mills was explicitly loaded.
Dr. Joan Meyers was already loaded.
There are appointments for Dr. Joan Meyers
Collection clear()'ed
There are now appointments for Dr. Joan Meyers
Collection loaded()'ed
There are now appointments for Dr. Joan Meyers
Collection loaded()'ed with MergeOption.OverwriteChanges
There are now appointments for Dr. Joan Meyers
Dr. Joan Meyers has appointments loaded.
Dr. Joan Meyers has appointments loaded.
Press <enter> to continue...

译注:书的结果有误,这是我(付灿)运行示例后的输出。

原理

  插入一些简单的数据到数据库之后,我们显式地禁用了延迟加载特征,因为我们要显式控制关联实体的加载。有两种方式禁用延迟加载:

    1、设置Context.Configuration对象的LazyLoadingEnabled属性为False。它会禁用上下文中所有实体对象的延迟加载。

    2、在每个实体类中移除导航属性的virtual修饰关键字。这种方法会禁用相应实体的延迟加载,这样就能让你显式控制延迟加载

  我们先获取一个Doctor实体。如果你使用了显式加载,这将是使用IsLoaded属性检查关联实体或实体集合是否加载的一个好实践。在代码中,我们检查doctor对象的appointments是否加载。如果没有,我们使用Load()方法加载它们。

  在foreach循环中,我们枚举了appointments,检查与它关联的doctor是否加载。注意输出,这时只有一个医生被加载,别的没有被加载。这是因为我们的第一个查询只获取了一个doctor。在获取appointmetns的过程中,实体框架会连接医生(doctor)和他的预约(appintments),这个过程被称为(非正式的)Relationship fixup(译注:这些概念虽然已经产生很多年,但中文资料关于它的介绍几乎没有,只看到一位兄弟把它翻译为“关系建立”,个人觉得它能表达这个词的含义,就借用了)。Relationship fixup 不会建立好所有的关联,特别是多对关联的实体

  我们打印出doctor关联实体集合appointments已加载的数量。然后我们调用Clear()方法,清空doctorJoan关联实体集合。这个方法会清除掉doctorJoan和appointments间的关系。有趣的是,它并不会把实例从内存中移除;这些实例仍在上下文中--它们只是不在跟Doctor实体连接

  令人奇怪的是,调用Load()方法重新加载appointemts后,从输出我们看到,doctorJoan的关联集合没有对象。发生了什么呢?原来是因为Load()方法需要使用一个控制如何加载实例进入上下文的参数。该参数的默认值是MergeOption.AppendOnly,它只是简单地把不存在上下文中的实体对象加载到上下文中。在我们示例中,没有没有真正地把实体对象从上下文中移除。在使用Clear()方法时,只是将实体对象从关联集合中移除,而没有从上下文中移除。当我们使用Load()方法重新加载时,由于使用了默认的MergeOption.AppendOnly选项,又没有发现新的实例,所有没有实体对象被加载到上下文中(译注:关联实体集合自然也不会添加,但注意这里的Load()方法是生成了SQL查询语句,产生了数据库交互,并从数据库获取了相应的数据的)。其它的合并选项包含:NoTracking,OverwriteChanges,和PreserveChages。当我们使用OverwriteChanges选项时,appointments出现在Doctor实体对象的关联集合Appointments中了。

  注意,我们在代码中是如何进入底层,通过ObjectContext上下文对象,访问实体框架中MergeOption行为的。MergeOption在DbContext中不被直接支持。你可能会回想起,我们使用实体框架时,有两个上下文对象可以使用。在实体框架6中,首选是使用DbContext。它提供了直观,易于使用的,遗留ObectContext上下文对象的外观模式。如代码中所示,可以通过显式转换,仍然可以使用ObjectContext。

  与AppendOnly一起,MegeOption类型公布了另外三个选项:

    1、NoTracking选项会关闭加载实例的对象状态跟踪。使用NoTracking选项,实体框架将不再跟踪对象的变化,同时也不再知道对象是否已经加载到上下文中。如果对象使用NoTracking选项加载,那么它可以被用于对象的导航属性上。NoTracking有一个额外的副作用。如果我们使用NoTracking选项加载一个Doctor实体,那么,使用Load()方法加载appointments时,不管默认行为AppendOnly,仍然会使用NoTracking。

    2、OverwriteChanges选项会使用从数据库获取的数据更新当前对象的值,实体框架会继续使用同一个实体对象。这个选项在你想放弃上下文中对实体对象的修改,并使用数据库中的数据来刷新它时特别管用。这个选项非常有用,例如,你的应用正在实现一个撤消操作的功能。

    3、PreserveChanges选项,本质上是OverwriteChanges选项的对立选项。当数据库中有改变时,它会更新实体对象的值。但是当内存里的值发生改变时,它不会更新实体对象的值。一个实体对象在内存中被修改,它不会被刷新。更准确地说,在内存中修改实体对象时,它的当前值(cruuent value)不会改变,但是,如果数据库有改变时,它的初始值(original value)会被更新。

  当你使用Load()方法时,这里有一些限制。实体状态为Added,Deleted,或者是Detached时,不能调用Load()方法

  无论在什么时候,只要想限制关联实体集合中实体的加载数量,Load()方法对性能的提升都有帮助。例如,我们的医生有大量的预约,但是在很多时候,他只能处理一部分。在极罕见的情况下会处理整个集合,我可以简单的调用Load()方法加载剩下的appointments实例。如代码清单5-32所示。

代码清单5-32.演示加载部分关联实体集合

 //演示先加载部分实体集合,然后再加载剩下的
using (var context = new EFRecipesEntities())
{
// 禁用延迟加载,因为我们要显式加载子实体
context.Configuration.LazyLoadingEnabled = false; //加载第一个doctor然后只附加一个appointment
var doctorJoan = context.Doctors.First(o => o.Name == "Joan Meyers");
context.Entry(doctorJoan).Collection(x => x.Appointments).Query().Take().Load(); //注意,这里IsLoaded返回False,因为所有的实体还没有被加载到上下文
var appointmentsLoaded = context.Entry(doctorJoan).Collection(x => x.Appointments).IsLoaded; Console.WriteLine("Dr. {0} has {1} appointments loaded.",
doctorJoan.Name,
doctorJoan.Appointments.Count()); //当我需要加载剩下的appointments,只需要简单的调用Load()来加载它们
context.Entry(doctorJoan).Collection(x => x.Appointments).Load();
Console.WriteLine("Dr. {0} has {1} appointments loaded.",
doctorJoan.Name,
doctorJoan.Appointments.Count());
}

代码清单5-31的输出如下:

Dr. Joan Meyers has  appointments loaded.
Dr. Joan Meyers has appointments loaded.

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

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