预先加载指在一个查询中加载所有的实体和关联的数据。延迟加载指使用时再加载关联的实体。预先加载是检索数据最高效的方法。尽管它从数据库中检索所有的数据,但是只访问一次数据库,避免了延迟加载与数据库的频繁通信。
预先加载通过ObjectQuery类的一个特殊方法:Include实现。这个方法接受一个字符串,表示要加载的导航属性。这个字符串被称为导航路径,因为它允许你加载整个相关对象图,而不仅仅是与你查询相关联的属性。
让我们以最简单的例子开始:查询有详细信息的订单:
var result = from o in ctx.Orders.Include("OrderDetails") select o;
OrderDetails是Order类的一个属性。如我们所说,Include的字符串参数是你可以加载一个完整属性图的路径。这在检索订单加上详细信息数据,即使与它们相关的每一个产品时都是非常有用的。您只需建立一个路径,以圆点(.)分隔属性即可。
var result = from o in ctx.Orders.Include("OrderDetails.Product") select o;
Include方法返回一个ObjectQuery<T>的实例,这意味着它可以与其他调用的Include方法和LINQ to Entities方法链接起来。
下面的代码片段加载订单和它们的详细信息,并且链接另一个Include也加载顾客:
var result = from o in ctx.Orders.Include("OrderDetails.Product").Include("Customer") select o;
这个查询需要明显的时间去执行,它返回给应用程序大量重复的数据(因为在SQL代码中生成了JOIN)。在这里没有必要展示SQL代码让你理解它有多么复杂。当然,你预先加载的相关实体越多,查询就会变的越复杂,执行的时间就会越长,网络上传送的数据也越多。
注意:
你看到Include方法可以和到目前为止你见过的所有方法链接使用。但是我们有两个原因强烈推荐你将Include放在查询的开始。一个是放在开始是查询更加直观。第二是,Include属于ObjectQuery<T>类,然而,LINQ扩展方法返回IEnumerable<T>。这意味着你已经应用了LINQ方法,就不能再使用Include,除非你显示将IEnumerable<T>转换回ObjectQuery<T>。
一对多关联加载什么样的数据,数据又是如何形成的呢?第一个答案是所有关联数据都被检索。如果你预先加载一个订单的详细信息,就没有办法筛选它们了。LINQ to Entities允许你将条件应用到预先加载的数据上,但是会被忽略。至于该如何,数据不能排序也不能投影。如果你需要应用任何修改到关联数据,你不得不求助于投影整个查询。
注意:
Include被翻译成OUTER JOIN的SQL语句。假设你需要订单和详细信息。如果一个订单没有任何详细信息,你也总是得到订单。这是正确的行为,因为你正在查找的就是订单。
一个往返过程检索数据太繁重,你可以尝试需要时再检索数据。这就是延迟加载做的事。
延迟加载一个关联实体或者实体列表,不需要学习新东西。你可以通过访问导航属性获得相关的实体。
假设你检索所有的订单,需要在详细信息之间虚幻。如果你没有预先取得它们,你可以通过遍历OrderDetails访问它们。这种情况和你访问指向单个实体的导航属性相同。例如,如果你想检索顾客信息,你可以访问Customer属性。
foreach (var order in ctx.Orders) { Console.WriteLine(order.OrderId + " " + order.Customer.Name); foreach (var detail in order.OrderDetails) { Console.WriteLine(detail.OrderDetailId + " " + detail.Quantity); } }
注意:
延迟加载默认是启用的。你可以通过设置ContextOptions.LazyLoadingEnabled上下文属性为True关闭。更重要的是,延迟加载时实体必须附加到上下文。如果你访问一个实体的导航属性,实体在生成实体的上下文作用域范围外,你将得到一个InvalidOperationException异常。
对属性getter的简单访问如何触发查询?如果你看了属性的代码,没有这个功能的追踪,那么这是如何发生的呢?还记得在第3章提到的代理吗?它就是这些问题的答案。
当上下文创建了代理类,它会检测导航到另一个实体的所有属性。它们中的每一个,如果标记了virtual,上下文就重写getter,注入所必需的代码来执行一个查询对数据库检索数据。如果属性不能被重写,代理就不能注入代码,延迟加载也就不能激活。当然,如果你关闭代理生成,就返回平常类,没有代理,没有延迟加载。
下图显示了一个简化内部代理的延迟加载的代码
延迟加载很有用,但是有些情况下你也不能依赖它,因为你可能禁用了代理生成或者在上下文之外。不要失望,EF仍然可以提供帮助。
手动延迟加载是动态检索一个属性而不使用延迟加载的一种方式。这个功能由对象上下文的LoadProperty方法实现,它有两个版本:泛型和非泛型。
作为泛型参数,泛型版本接受实体类型的属性必须在数据库中检索的到。然后对象加上一个将要被加载属性拉姆达表达式:
void LoadProperty<T>(T entity, Expression<Func<T, object>> selector);
非泛型版本接受两个参数。一个是实体,另一个是要加载的属性:
void LoadProperty(object entity, string navigationProperty);
下面列出手动加载每个订单的详细信息:
//泛型检索 ctx.LoadProperty<Order>(order, o => o.OrderDetails); //非泛型检索 ctx.LoadProperty(order, "OrderDetails");
为了让LoadProperty工作,主实体必须附加到上下文。如果位于上下文之外,你可以新建一个,附加上实体,然后加载属性。
在分层架构中,你可以将这个方法放在下层结构,以便数据访问仍然保持封装。看个例子:
public void LoadProperty<T>(T entity, Expression<Func<T, object>> selector) where T : class { using (var ctx = new OrderITEntities()) { ctx.CreateObjectSet<T>().Attach(entity); ctx.LoadProperty<T>(entity, selector); } }你需要了解的关于EF加载的问题就这些。
选择正确的加载策略在一个程序中产生很大的不同。通常情况下,应用程序的开发不牢记这一点,最终你预先使用大量的查询加载所有的数据或者在运行时使用小量的查询检索相关的数据。
第二种情况是最危险的。我们已经看到了只检索主实体的应用程序,所有的关联数据在运行时加载。一切工作,因为透明的延迟加载。但是至少性能很差,如果不是灾难的话。
一般来说,预先加载比较好。但是你可能发现了在某些情况下你必须加载关联的实体。例如,你可能只想为过去7天的订单加载详细信息。在这种情况下,根据需要加载详细信息可能是一个好的选择。更糟的是,预先加载生成的SQL 并不总是表现出色。在一些情况下,EF生成的SQL包含了无用的OUTER JOIN命令或者比需要检索更多的列。
确定正确的抓取策略是一个测试问题,要逐案分析。
第四章终于结束了,第五章先暂停一下,我会将前面的知识总结一下和colorbox结合写个例子。