[翻译] - <Entity Framework> - 载入相关对象
纯属学习上的记录, 非专业翻译, 如有错误欢迎指正!
原文地址:http://msdn.microsoft.com/en-us/library/gg715120(v=vs.103)
本节将讨论载入相关对象的不同方式. EF 中的导航属性提供了一种访问一个集合或两个实体间的关系, 它会返回对一个对象的引用(如果两者之间的多重性是一对一或者一对零或一的话)或对一个集合的引用(如果两者之间的多重性是一对多的话). 在接下来的例子中, Department 类中的 Courses 属性和 Course 类中的 Department 属性就是导航属性.
1 public class Department 2 { 3 public Department() 4 { 5 this.Courses = new HashSet<Course>(); 6 } 7 8 // Primitive properties 9 public int DepartmentID { get; set; } 10 public string Name { get; set; } 11 12 // Navigation properties 13 public virtual ICollection<Course> Courses { get; private set; } 14 } 15 16 17 public class Course 18 { 19 // Primitive properties 20 public int CourseID { get; set; } 21 public string Title { get; set; } 22 public int Credits { get; set; } 23 public int DepartmentID { get; set; } 24 25 // Navigation properties 26 public virtual Department Department { get; set; } 27 }
该实例基于 School 概念模型.
完全加载相关实体
当你准确的知道你的程序需要的实体关系图时, 你可以使用 DbQuery 类中的 Include 方法或 DbExtensions 类中的 Include 方法的一个重载通过定义一个查询路径来控制哪些相关实体将会作为初始查询的一部分返回. 当你定义了一个查询路径后, 将只要对数据库进行一次返回, 便能返回一个包含所有查询路径中定义的要返回的实体的结果集. 此外, 在查询路径中定义的相关类型将由查询返回的对象填充. 例如, 下面的查询将载入 Department 对象和所有与 Department 相关的 Course 对象.
1 // 载入所有部门和其相关的课程 2 3 var departments1 = context.Departments 4 .Include(d => d.Courses) 5 .ToList(); 6 // 载入一个部门和其相关课程 7 var department1 = context.Departments 8 .Where(d => d.Name == " Mathematics") 9 .Include(d => d.Courses) 10 .FirstOrDefault(); 11 // 载入所有部门, 并通过字符串来指定需要载入的相关对象 12 var departments2 = context.Departments 13 .Include("Courses") 14 .ToList(); 15 // 载入一个部门, 并通过字符串来指定需要载入的相关对象 16 var department2 = context.Departments 17 .Where(d => d.Name == "Mathemtaics") 18 .Include("Courses") 19 .FirstOrDefault();
同时, 完全载入多个层级的实体也是可行的. 例如:
1 // 载入所有部门和其相关课程, 及课程相关的教师. 2 var departments = context.Departments 3 .Include(d => d.Courses.Select(c => c.Instructors)) 4 .ToList(); 5 6 7 // 载入所有大于 3 年级的学生年级, 同时载入相关课程和部门 8 var StudentGrades = context.StudentGrades.Where(s => s.Grade > 3) 9 .Include(s => s.Course.Department) 10 .ToList(); 11 }
注意, 到目前为止还不支持对载入的相关实体进行过滤.
使用查询路径时, 看似简单的对数据库的数据查询会导致复杂的命令执行. 这是因为, 为了能够在一次查询中返回相关的对象, 一个或多个 join 命令将会被执行, 从而导致从数据库中返回的实体集中的相关实体中存在冗余数据. 在对复杂模型的查询时, 如: 一个带继承的实体, 或一个包括多对多关心的路径, 这个复杂性将变得更大.
当一个查询路径包括太多的相关对象, 或结果中包含太多的行数据时, 数据库可能无法完成该次查询. 这是因为查询所需要的元数据存储空间超过了数据库的容量. 当这种情况发生时, 你可以通过只载入必须的相关对象来降低查询的复杂性, 或者允许延迟查询. 如果通过优化以后, 在执行复杂查询时仍会经常发现超时的现象, 可以考虑通过设置 CommandTimeout 属性来增加超时时间.
延迟加载相关实体
EF 支持对相关实体的延迟加载. 在这种加载方式中, 当你访问一个导航属性时, 相关实体将会自动的被从数据库中加载过来. 需要注意的是, 当使用延迟加载时, 你每访问一个导航属性, 只要该属性所要求返回的实体还没有被载入到上下文对象中, 那么一个单独的查询就将在数据库上执行. 在 EF 4.1 和更新的版本中, 延迟加载默认是启用的. 可以通过上下文对象中的 Configuration 属性来禁用延迟加载. 如: context.Configuration.LazyLoadingEnabled = false. 为了使 POCO 实体类型支持延迟加载, 它们必须满足 Requirements for Creation POCO Proxies (Entity Framework) ([翻译] - <Entity Framewrok> - 创建 POCO 代理需满足的条件)中描述的创建延迟加载代理的先决条件.
例如, 在使用 Deparment 实体类时, 其中的相关实体对象将会在该对象的导航属性第一次被访问时载入.
1 // 如果延迟加载被禁用的话, 将不会有 Courses 被载入到 department 对象中 2 3 foreach (Course c in department.Courses) 4 Console.WriteLine("CourseID: {0} ", c.CourseID);
延迟加载代理创建的一个先决条件是: 导航属性必须被声明为 virtual (在 Visual Basic 中是 Overridabel). 如果你不想所有的导航属性都使用延迟加载功能, 那么你可以通过将一些属性声明为非虚来禁止该功能的使用.
延迟加载可以和完全加载混合着使用. 在这种情况下, 可以通过查询路径定义出基本的数据关系图, 而实际操作时额外的 (即不在查询路径中的) 实体在用到的时候可以通过延迟加载载入.
使用延迟加载时注意考虑一下情况:
- 延迟加载支持返回单一实体或实体集的导航属性;
- 如果延迟加载被启用, 一个对象不会被加载第二次;
- 延迟加载用于支持处于 Detached(分离的) 状态的实体. 在这种情况下, 相关的实体也会以 Detached 的状态载入;
- 延迟加载行为由上下文对象定义, 其中上下文对象用于检索数据库中的对象, 同时实体对象的添加也必须通过它进行. 故延迟加载行为在上下文对象被回收后将无法改变, 且任何延迟加载相关操作都将失败;
- 序列化实体对象时, 建议停用延迟加载. 否则, 延迟加载将会被触发, 且将会有多余的对象被序列化.
精确加载相关实体
即使禁用了延迟加载功能, 也可以通过直接调用相关实体对象上的 Load 方法来实现延迟加载. 如:
1 using (var context = new SchoolEntities()) 2 { 3 var department = context.Departments.Find(1); 4 var course = context.Courses.Find(2); 5 // 使用 Lambda 表达式来载入与 course 对象相关的 Department 实体 6 context.Entry(course).Reference(c => c.Department).Load(); 7 // 通过一个字符串来载入与 course 对象相关的 Department 实体 8 context.Entry(course).Reference("Department").Load(); 9 // 载入与指定 Department 对象相关的 Course 对象 10 context.Entry(department).Collection(d => d.Courses).Load(); 11 // 通过一个字符串定义的关系载入与指定 Department 对象相关的 Course 对象 12 context.Entry(department).Collection("Courses").Load(); 13 }
当一个实体的导航属性指向另一个单一实体时, 用 Reference 方法, 而当其指向另一个实体的集合时, 则要用 Collection 方法.
精确加载相关实体时进行过滤
DbCollectionEntry 和 DbReferenceEntry 类中的 Query 方法提供了对 EF 载入相关实体时使用的基本查询的访问. 通过该方法, 你可以使用 LINQ 表达式在查询执行前为其提供过滤器. Query 可以与引用或集合类导航属性一起使用, 不过大多与集合一起使用, 因为通过它我们可以只载入集合的一部分实体, 例如:
1 using (var context = new SchoolEntities()) 2 { 3 var department = context.Departments.Find(1); 4 // 载入与 department 相关的 Course 对象中 Title 以 M 开头的对象 5 context.Entry(department) 6 .Collection(c => c.Courses) // Collection 方法也接受一个字符串参数: .Collection("Courses") 7 .Query() 8 .Where(c => c.Title.StartsWith("M")) 9 .Load(); 10 }
使用 Query 方法时最好禁用延迟加载功能. 若延迟加载被启用, 集合中的所有对象在过滤查询之前或之后将将由延迟加载机制自动加载.注意, 尽管关系参数可以通过一个字符串而不是 Lambda 表达式提供, 但当使用字符串作为参数时, 返回的 IQueryable 结果集并非是泛型的, 在对其进行任何操作前需要执行 Cast 方法.
使用 Query 方法对实体进行计数但不加载他们
如果你想知道数据库中有多少实体对象与某个对象相关联, 但又不想消耗资源载入这些实体对象, 使用将 Query方法和 LINQ 的 Count 方法一起使用可以达到这个效果. 如:
1 using (var context = new SchoolEntities()) 2 { 3 var department = context.Departments.Find(1); 4 // 对属于 department 对象的 Course 对象进行计数 5 var courseCount = context.Entry(department) 6 .Collection(c => c.Courses) 7 .Query() 8 .Count(); 9 }
性能方面的注意事项
当你在选择载入相关实体的方式时, 你必须从数据量, 连接到数据库所用的时间, 数据的多样性和采用单一查询的复杂性这几个方面来考虑每种方法的行为. 完全加载会将所查询的实体和他们的所有相关实体通过一次查询全部返回. 这意味着, 仅通过一次数据库连接, 大量的数据将会由一次基本查询返回. 另一方面, 查询路径将导致一个更负责的查询, 因为对数据库进行的查询时需要添加额外的 join 命令.
精确加载和延迟加载使你能够将相关对象数据加载这件事推迟到需要具体的数据时才做. 这将生成一个较不复杂, 且返回数据量较少的查询. 但是, 每次对相关对象的成功加载需要进行一次数据库连接和执行一次数据查询. 使用延迟加载时, 数据库连接将在每个相关对象还没别载入上下文对象中的导航属性被访问时执行. 如果你关心由初始查询返回的相关实体或相关实体从数据库中加载到上下文对象中的时间, 你应该考虑禁用延迟加载.