数据点

揭开 Entity Framework 策略的面纱:加载相关数据

Julie Lerman

Julie Lerman
上个月的数据点专栏中,我提供了一些关于从数据库优先、模型优先和代码优先选项中选择建模工作流策略的高级指导。本月,我将讲述您需要进行的另一个重要选择:如何从数据库中检索相关数据。您可以使用预先加载、显式加载、延迟加载,甚至是查询投影。

但这不会是个一次性决定,因为应用程序中的不同情形可能需要不同的数据加载策略。因此,最好了解每一个策略,以便您可以为所做工作选择合适的策略。

举个例子,假设您有一个跟踪家庭宠物的应用程序,您的模型具有一个 Family 类和一个 Pet 类,而且 Family 和 Pet 之间是一对多的关系。假设您想要检索关于某个家庭及其宠物的信息。

在下一专栏中,我将继续讲述这一系列,通过使用 LINQ to Entities、实体 SQL 以及上述各个选项的变体来解决在查询实体框架时面临的各种选择。但在本专栏中,我只对各个示例使用 LINQ to Entities。

一次数据库访问中的预先加载

预先加载可以让您仅通过一次访问便可从数据库返回所有数据。实体框架提供 Include 方法来实现此目标。Include 用字符串表示到相关数据的导航路径。下面是 Include 方法的一个示例,它将返回一些图表,每个图表都包含一个家庭及其宠物的集合:

from f in context.Families.Include("Pets") select f

如果您的模型还有一个名为 VetVisit 的实体,而且该实体与 Pet 之间存在一对多的关系,则您可以一次返回家庭、其宠物及其宠物的兽医诊查次数:

from f in context.Families.Include("Pets.VetVisits") select f

预先加载的结果将显示为对象图表,如图 1 所示。

图 1 预先加载查询返回的对象图表

Id Name Pets          
    Id Name Type VetVisits    
2 LermanJ 2 Sampson Dog 1 2/1/2011 Excellent
5 4/1/2011 Nail Clipping
4 Sissy Cat 1 3/2/2011 Excellent
3 GeigerA 3 Pokey Turtle 3 2/5/2011 Excellent
4 Riki Cat 6 4/8/2011 Excellent

Include 方法非常灵活。 您可以一次使用多个导航路径,也可以导航到父实体或通过多对多关系来实现。

使用 Include 方法预先加载确实非常方便,但是如果使用过度 — 在一次查询中使用许多 Include 或在一个 Include 中使用许多导航路径 — 则会迅速降低查询性能。 实体框架构建的本机查询会有许多联接,而且为了方便返回您所请求的图表,数据库结果的形状可能会过于复杂或返回过多的结果。 这并不是说您应该避免使用 Include,而是说您应该简明扼要地描述您的实体框架查询,以确保不会生成效率不高的查询。 如果本机查询特别棘手,则您应该为生成查询的应用程序区域重新考虑查询策略。

其他数据库访问中的延迟加载

通常,检索数据时,您并不是立即想要或需要相关数据,或者可能并不想显示所有结果的相关数据。 例如,您可能需要将所有家庭加入您的应用程序中,但之后只为其中一部分人检索宠物。 在这种情况下,使用 Include 方法为所有人预先加载所有宠物有意义吗? 很可能没有。

实体框架提供了两种在事后加载相关数据的方法。 第一种方法叫做延迟加载,在适当设置的情况下,它可自动运行。

使用延迟加载,您只需对相关数据进行一些引用,实体框架即会检查是否已加载到内存中。 如果没有,实体框架就会在幕后创建并执行一个查询,填充相关数据。

例如,如果您执行一个查询来获取一些 Family 对象,然后通过指定 Pets 属性触发实体框架来获取其中一个家庭的 Pets,实体框架就会为您检索 Pets:

var theFamilies= context.Families.ToList();
var petsForOneFamily = theFamilies[0].Pets;

在实体框架中,使用 ObjectContext ContextOptions.LazyLoadingEnabled 属性来启用或禁用延迟加载。 默认情况下,Visual Studio 将定义新创建的模型,将 LazyLoadingEnabled 设置为 true,结果将是默认情况下对新模型启用延迟加载。

实例化上下文时默认情况下启用延迟加载对应用程序来说是个不错的选择,但是对于不了解此行为的开发人员来说却是个难题。 您可能会触发对数据库的其他访问,却没有意识到这一点。 您是否在使用延迟加载由您自己来判断,您可以根据需要明确地选择使用或不用延迟加载 — 通过将 LazyLoadingEnabled 设置为 true 或 false 来在您的代码中启用或禁用延迟加载。

延迟加载由 EntityCollection 和 EntityReference 类驱动,因此,当您使用 Plain Old CLR Object (POCO) 类时不会默认启用延迟加载,即使 LazyLoadingEnabled 为 true 也是如此。 但是,实体框架动态代理行为(可通过将导航属性设置为 virtual 或 Overridable 来触发)将创建一个运行时代理,该代理使您的 POCO 也可以使用延迟加载。

延迟加载是实体框架中的一项强大功能,但仅当您了解它何时处于活动状态以及了解何时启用恰当和何时启用不恰当时才是如此。 例如,MSDN 杂志 文章“使用 Entity Framework 减少 SQL Azure 的网络延迟”(msdn.microsoft.com/magazine/gg309181) 重点讲述从内部部署服务器对云数据库使用延迟加载的性能方面的意义。 分析由实体框架执行的数据库查询是您策略中的重要部分,有助于您选择恰当的加载策略。

了解何时禁用延迟加载也很重要。 如果将 LazyLoadingEnabled 设置为 false,则上一示例中对 theFamilies[0].Pets 的引用不会触发数据库查询,并且会报告该家庭没有宠物,即使数据库中记录有宠物也会如此。 因此,如果您依赖延迟加载,一定要确保您启用了该功能。

其他数据库访问中的显式加载

您可能希望禁用延迟加载,而在加载相关数据时进行更显式的控制。 除了使用 Include 方法进行显式加载以外,实体框架也允许您使用其中一个 Load 方法有选择地显式检索相关数据。

如果您使用默认代码生成模板生成实体类,这些类将从 EntityObject 进行继承,相关数据则显示在 EntityCollection 或 EntityReference 中。 这两种类型都具有 Load 方法,您可以调用该方法强制实体框架检索相关数据。 下面是加载 Pets 对象的 EntityCollection 的一个示例。 请注意,Load 没有返回值:

var theFamilies = context.Families.ToList();
theFamilies[0].Pets.Load();
var petsForOneFamily = theFamilies[0].Pets;

实体框架将创建并执行一个查询,该查询会填充相关属性 — Family 的 Pets 集合,然后您就可以对 Pets 进行操作了。

显式加载的第二种方法是从 ObjectContext 而不是 EntityCollection 或 EntityReference 加载。 如果您依赖实体框架中的 POCO 支持,您的导航属性将不会是 EntityCollections 或 EntityReferences,因此也就不会有 Load 方法。 相反,您可以使用 ObjectContext.LoadProperty 方法。 LoadProperty 使用泛型来确定加载源的类型,然后使用 lambda 表达式来指定要加载的导航属性。 下面是使用 LoadProperty 为特定人员实例检索 Pets 的一个示例:

context.LoadProperty<Family>(familyInstance, f => f.Pets)

加载的替代方法:查询投影

不要忘记,您还可以在查询中使用投影。 例如,您可以编写一个查询来检索实体,但是要对检索到的相关数据进行筛选:

var famsAndPets=from family in context.Families
  select new {family,Pets=family.Pets.Any(p=>p.Type=="Reptile")};

这将返回所有家庭以及那些拥有爬行动物的家庭的宠物,所有这些都在一次数据库访问中完成。 但是,famsAndPets 查询返回的结果不是家庭及其宠物的图表,而是一组具有两个属性的匿名类型:一个是 Family 属性,一个是 Pets 属性(请参见图 2)。

图 2 具有 Family 和 Pets 属性的投影匿名类型

Family Pets      
Id Name Id Name Type
2 LermanJ      
3 GeigerA 3 Pokey Turtle
4 Riki Cat

评估优点和缺点

现在,您了解了四种检索相关数据的策略。 在您的应用程序中,它们不一定是互相排斥的。 您很可能能够找出在您的应用程序中的各种情形中使用上述不同功能的原因。 您应该先考虑各个策略的优点和缺点,然后再为各种情形选择恰当的策略。

对于那些您事先知道要为查询的所有核心数据显示相关数据的情形来说,使用 Include 方法进行预先加载很有用。 但是要记住两个潜在的弊端。 如果您的 Include 或导航路径太多,实体框架可能会生成效率不高的查询。 此外,由于使用 Include 易于进行编码,因此,您还应注意返回过多不必要的相关数据的情况。

延迟加载可以在幕后非常方便地检索相关数据,以便响应那些仅涉及相关数据的代码。 它也会使编码变得更为简单,但您应注意它导致的与数据库的交互次数。 只需一次或两次数据库访问时,它可能会导致 40 次。

显式加载使您可以更大程度地控制检索相关数据的时间(以及检索哪些数据),但是如果您不了解此选项,则您的应用程序了解到的数据库中存在的相关数据信息可能是有误的。 许多开发人员都认为显式加载很繁琐,而另一些开发人员则很乐意通过它来实现精细的控制。

在查询中使用投影可以使您做到两全其美 — 在一个查询中有选择地检索相关数据。 不过,如果从这些投影返回匿名类型,您可能会发现它们更加难以处理,因为实体框架状态管理器没有跟踪对象,所以不会更新这些对象。

图 3 显示了一个决策流程图,您第一次选择策略时可参照此图。 但是,您还应考虑性能问题。 利用查询分析和性能测试工具,可以确保您作出正确的数据加载选择。

Your First Pass at Loading Strategy Decisions

图 3 第一次选择加载策略

Julie Lerman是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。她是《Programming Entity Framework》(O'Reilly Media,2010)一书的作者,该书受到广泛称赞,她的博客地址是 thedatafarm.com/blog。请关注她的 Twitter:twitter.com/julielerman

衷心感谢以下技术专家对本文的审阅:Tim Laverty