EF 4、5 和 6 的性能注意事项Performance considerations for EF 4, 5, and 6

由大卫·奥班多,埃里克·丁格和其他人

发布时间:2012 年 4 月

上次更新时间:2014 年 5 月


1. 介绍1. Introduction

对象关系映射框架是一种为面向对象的应用程序中的数据访问提供抽象的便捷方法。 对于 .NET 应用程序,Microsoft 推荐的 O/RM 是实体框架。 然而,对于任何抽象,性能都可能成为一个问题。

编写本白皮书是为了显示使用实体框架开发应用程序时的性能注意事项,让开发人员了解可能影响性能的实体框架内部算法,并提供调查和改进使用实体框架的应用程序的性能的提示。 Web 上已有许多有关性能的好主题,我们还尝试尽可能指向这些资源。

性能是一个棘手的话题。 本白皮书旨在作为资源,帮助您为使用实体框架的应用程序做出与性能相关的决策。 我们包括一些测试指标来演示性能,但这些指标并非旨在作为您在应用程序中看到的性能的绝对指标。

出于实际目的,本文档假定实体框架 4 在 .NET 4.0 下运行,实体框架 5 和 6 在 .NET 4.5 下运行。 实体框架 5 的许多性能改进都位于提供 .NET 4.5 的核心组件中。

实体框架 6 是带外版本,不依赖于随 .NET 一起发货的实体框架组件。 实体框架 6 在 .NET 4.0 和 .NET 4.5 上都工作,可以为尚未从 .NET 4.0 升级但希望在其应用程序中使用最新实体框架位的用户提供巨大的性能优势。 当本文档提到实体框架 6 时,它指的是本文编写时可用的最新版本:版本 6.1.0。

2. 冷与暖查询执行2. Cold vs. Warm Query Execution

首次对给定模型进行任何查询时,实体框架会在幕后执行大量工作来加载和验证模型。 我们经常将第一个查询称为”冷”查询。针对已加载模型的进一步查询称为”暖”查询,并且速度更快。

让我们从高级视图来了解使用实体框架执行查询时所花费的时间,并了解实体框架 6 中情况正在改善。

第一次查询执行 = 冷查询

代码用户写入操作EF4 性能影响EF5 性能影响EF6 性能影响
using(var db = new MyContext())
{
上下文创建中型中型
var q1 =
from c in db.Customers
where c.Id == id1
select c;
查询表达式创建
var c1 = q1.First();LINQ 查询执行- 元数据加载:高但缓存
- 视图生成:可能非常高但缓存
- 参数评估:中等
- 查询翻译:中等
- 具体生成:中等但缓存
- 数据库查询执行:可能很高
• 连接。打开
• 命令.执行读取器
• 数据阅读器。阅读
对象具体化:中等
- 身份查找:中等
- 元数据加载:高但缓存
- 视图生成:可能非常高但缓存
- 参数评估:低
- 查询翻译:中等但缓存
- 具体生成:中等但缓存
- 数据库查询执行:可能很高(在某些情况下查询更好)
• 连接。打开
• 命令.执行读取器
• 数据阅读器。阅读
对象具体化:中等
- 身份查找:中等
- 元数据加载:高但缓存
- 视图生成:中等但缓存
- 参数评估:低
- 查询翻译:中等但缓存
- 具体生成:中等但缓存
- 数据库查询执行:可能很高(在某些情况下查询更好)
• 连接。打开
• 命令.执行读取器
• 数据阅读器。阅读
对象具体化:中等(快于 EF5)
- 身份查找:中等
}连接.关闭

第二次查询执行 = 暖查询

代码用户写入操作EF4 性能影响EF5 性能影响EF6 性能影响
using(var db = new MyContext())
{
上下文创建中型中型
var q1 =
from c in db.Customers
where c.Id == id1
select c;
查询表达式创建
var c1 = q1.First();LINQ 查询执行- 元数据加载查找:高但缓存
- 查看生成查找:可能非常高但缓存
- 参数评估:中等
- 查询翻译查找:中等
- 具体生成查找中等但缓存
- 数据库查询执行:可能很高
• 连接。打开
• 命令.执行读取器
• 数据阅读器。阅读
对象具体化:中等
- 身份查找:中等
- 元数据加载查找:高但缓存
- 查看生成查找:可能非常高但缓存
- 参数评估:低
- 查询翻译查找:中但缓存
- 具体生成查找中等但缓存
- 数据库查询执行:可能很高(在某些情况下查询更好)
• 连接。打开
• 命令.执行读取器
• 数据阅读器。阅读
对象具体化:中等
- 身份查找:中等
- 元数据加载查找:高但缓存
- 查看生成查找:中等但缓存
- 参数评估:低
- 查询翻译查找:中但缓存
- 具体生成查找中等但缓存
- 数据库查询执行:可能很高(在某些情况下查询更好)
• 连接。打开
• 命令.执行读取器
• 数据阅读器。阅读
对象具体化:中等(快于 EF5)
- 身份查找:中等
}连接.关闭

有几种方法可以降低冷查询和暖查询的性能成本,我们将在下一节中介绍这些查询。 具体来说,我们将考虑通过使用预生成的视图来降低冷查询中模型加载的成本,这将有助于缓解在生成视图期间遇到的性能难题。 对于暖查询,我们将介绍查询计划缓存、无跟踪查询和不同的查询执行选项。

2.1 什么是视图生成?2.1 What is View Generation?

为了了解什么是视图生成,我们必须首先了解什么是”映射视图”。 映射视图是映射中为每个实体集和关联指定的转换的可执行表示形式。 在内部,这些映射视图采用 CQT(规范查询树)的形状。 有两种类型的映射视图:

  • 查询视图:这些视图表示从数据库架构到概念模型所需的转换。
  • 更新视图:这些视图表示从概念模型到数据库架构所需的转换。

请记住,概念模型可能以各种方式与数据库架构不同。 例如,一个表可用于存储两种不同实体类型的数据。 继承和非常规映射在映射视图的复杂性中起着一定的作用。

基于映射规范计算这些视图的过程就是我们所说的视图生成。 视图生成可以在加载模型时动态进行,也可以在生成时使用”预生成的视图”进行动态生成;后者以实体 SQL 语句的形式序列化到 C#或 VB 文件。

生成视图时,也会验证视图。 从性能角度来看,视图生成成本的绝大多数实际上是对视图的验证,这可确保实体之间的连接有意义,并且对所有支持的操作具有正确的基数。

执行对实体集的查询时,查询将与相应的查询视图组合,并且此合成的结果通过计划编译器运行,以创建备份存储可以理解的查询的表示形式。 对于 SQL 服务器,此编译的最终结果将是 T-SQL SELECT 语句。 首次对实体集执行更新时,更新视图将通过类似的过程运行,以将其转换为目标数据库的 DML 语句。

2.2 影响视图生成性能的因素2.2 Factors that affect View Generation performance

视图生成步骤的性能不仅取决于模型的大小,还取决于模型的互连程度。 如果两个实体通过继承链或关联连接,则它们表示已连接。 同样,如果两个表通过外键连接,则它们被连接。 随着架构中连接的实体和表数量的增加,视图生成成本也会增加。

在最坏的情况下,我们用于生成和验证视图的算法呈指数级,尽管我们使用一些优化来改进这一点。 似乎对性能产生负面影响的最大因素是:

  • 模型大小,指实体的数量和这些实体之间的关联量。
  • 模型复杂性,特别是涉及大量类型的继承。
  • 使用独立关联,而不是外国密钥关联。

对于小型、简单的模型,成本可能足够小,无需使用预生成的视图。 随着模型大小和复杂性的增加,有多种选项可用于降低视图生成和验证的成本。

2.3 使用预生成的视图减少模型加载时间2.3 Using Pre-Generated Views to decrease model load time

有关如何在实体框架 6 上使用预生成的视图的详细信息,请访问预生成的映射视图

2.3.1 使用实体框架电动工具社区版的预生成视图2.3.1 Pre-Generated views using the Entity Framework Power Tools Community Edition

您可以使用实体框架 6 电动工具社区版生成 EDMX 和代码优先模型的视图,通过右键单击模型类文件并使用实体框架菜单选择”生成视图”。 实体框架电动工具社区版仅适用于 DbContext 派生的上下文。

2.3.2 如何使用预生成的视图与 EDMGen 创建的模型2.3.2 How to use Pre-generated views with a model created by EDMGen

EDMGen 是一个实用程序,它与 .NET 一起提供,并与实体框架 4 和 5 一起工作,但与实体框架 6 无关。 EDMGen 允许您从命令行生成模型文件、对象层和视图。 其中一个输出是您选择的语言的”视图”文件,VB 或 C#。 这是一个代码文件,其中包含每个实体集的实体 SQL 代码段。 要启用预生成的视图,只需将文件包含在项目中即可。

如果手动编辑模型的架构文件,则需要重新生成视图文件。 可以通过使用 /mode:ViewGeneration标志运行 EDMGen 来执行此操作。

2.3.3 如何使用预生成的视图与 EDMX 文件2.3.3 How to use Pre-Generated Views with an EDMX file

您还可以使用 EDMGen 为 EDMX 文件生成视图 - 前面引用的 MSDN 主题介绍如何添加预构建事件来执行此操作 - 但这样做很复杂,在某些情况下,这是不可能的。 当模型位于 edmx 文件中时,通常使用 T4 模板生成视图会更容易。

ADO.NET团队博客有一篇文章,描述如何使用 T4 模板生成视图 (。 < https://docs.microsoft.com/archive/blogs/adonet/how-to-use-a-t4-template-for-view-generation>) 此帖子包含一个模板,可以下载并添加到您的项目中。 模板是为实体框架的第一个版本编写的,因此不能保证它们能够与最新版本的实体框架配合使用。 但是,可以从可视化工作室库下载实体框架 4 和 5 的最新视图生成模板集:

如果您使用的是实体框架 6,则可以从 中的http://visualstudiogallery.msdn.microsoft.com/18a7db90-6705-4d19-9dd1-0a6c23d0751f可视化工作室库获取视图生成 T4 模板。

2.4 降低视图生成成本2.4 Reducing the cost of view generation

使用预生成的视图可将视图生成成本从模型加载(运行时间)移动到设计时间。 虽然这提高了运行时的启动性能,但在开发时,您仍将经历视图生成的痛苦。 还有其他几种技巧可以帮助降低在编译时间和运行时生成视图的成本。

2.4.1 使用外键关联降低视图生成成本2.4.1 Using Foreign Key Associations to reduce view generation cost

我们看到许多案例,将模型中的关联从独立协会切换到外国密钥关联,从而大大缩短了在视图生成中花费的时间。

为了证明这一改进,我们使用 EDMGen 生成了两个版本的 Navision 模型。 注意:有关 Navision 模型的说明,请参阅附录 C。 Navision 模型对本练习非常感兴趣,因为它拥有大量实体和它们之间的关系。

此非常大的模型的一个版本是使用外键关联生成的,另一个版本是使用独立关联生成的。 然后,我们安排为每个模型生成视图需要多长时间。 实体框架 5 测试使用类 EntityViewGenerator 的 GenerateViews() 方法生成视图,而实体框架 6 测试使用类存储映射项目集合中的 GenerateViews() 方法。 这是因为实体框架 6 代码库中的代码重组。

使用实体框架 5,使用外键生成模型的视图在实验室机器中花了 65 分钟。 不知道为使用独立关联的模型生成视图需要多长时间。 在实验室中重新启动计算机以安装每月更新之前,我们让测试运行了一个多月。

使用实体框架 6,使用外键生成模型的视图生成需要 28 秒在同一实验室机器中。 使用独立关联的模型的视图生成需要 58 秒。 实体框架 6 对其视图生成代码所做的改进意味着许多项目不需要预生成的视图来获取更快的启动时间。

请务必指出,实体框架 4 和 5 中的预生成视图可以使用 EDMGen 或实体框架电动工具完成。 对于实体框架 6 视图生成,可以通过实体框架电源工具完成,也可以以编程方式完成,如预生成的映射视图中所述。

2.4.1.1 如何使用外键而不是独立关联2.4.1.1 How to use Foreign Keys instead of Independent Associations

在 Visual Studio 中使用 EDMGen 或实体设计器时,默认情况下,您将获得 PK,并且只需单个复选框或命令行标志即可在 SDK 和 I 之间切换。

如果您有一个大型 Code First 模型,则使用独立关联对视图生成的影响相同。 可以通过将外键属性包括在从属对象的类中来避免这种影响,尽管一些开发人员会认为这会污染其对象模型。 您可以在 中找到http://blog.oneunicorn.com/2011/12/11/whats-the-deal-with-mapping-foreign-keys-using-the-entity-framework/有关此主题的详细信息。

使用时操作
实体设计器在两个实体之间添加关联后,请确保具有引用约束。 引用约束告诉实体框架使用外键而不是独立关联。 有关其他详细信息,<https://docs.microsoft.com/archive/blogs/efdesign/foreign-keys-in-the-entity-framework>请访问
EDMGen当使用 EDMGen 从数据库生成文件时,您的外键将受到尊重并添加到模型中。 有关 EDMGen 公开的不同选项的更多信息,请访问http://msdn.microsoft.com/library/bb387165.aspx
Code First有关在使用代码优先时如何将外键属性包含在从属对象上的信息,请参阅代码优先约定主题的”关系约定”部分。

2.4.2 将模型移动到单独的装配体2.4.2 Moving your model to a separate assembly

当模型直接包含在应用程序的项目中,并且通过预生成事件或 T4 模板生成视图时,每当重建项目时,即使模型未更改,也会进行视图生成和验证。 如果将模型移动到单独的程序集并从应用程序的项目中引用它,则可以对应用程序进行其他更改,而无需重新生成包含模型的项目。

注意: 将模型移动到单独的程序集时,请记住将模型的连接字符串复制到客户端项目的应用程序配置文件中。

2.4.3 禁用基于 edmx 的模型验证2.4.3 Disable validation of an edmx-based model

EDMX 模型在编译时进行验证,即使模型保持不变。 如果模型已过验证,则可以通过在属性窗口中将”在生成上验证”属性设置为 false 来禁止编译时验证。 更改映射或模型时,可以临时重新启用验证以验证更改。

请注意,实体框架 6 的实体框架设计器的性能得到了提高,”在生成时验证”的成本比设计器的早期版本要低得多。

3 实体框架中的缓存3 Caching in the Entity Framework

实体框架具有以下形式的缓存内置:

  1. 对象缓存 - 内置到 ObjectContext 实例中的 ObjectStateManager 在使用该实例检索的对象的内存中跟踪该对象。 这也称为第一级缓存。
  2. 查询计划缓存 - 多次执行查询时重用生成的存储命令。
  3. 元数据缓存 - 跨与同一模型的不同连接共享模型的元数据。

除了 EF 开箱即用的缓存外,一种称为包装提供程序的特殊ADO.NET数据提供程序还可用于扩展实体框架,并缓存从数据库检索的结果(也称为二级缓存)。

3.1 对象缓存3.1 Object Caching

默认情况下,当在查询结果中返回实体时,就在 EF 实现该实体之前,ObjectContext 将检查具有相同密钥的实体是否已加载到其 ObjectStateManager 中。 如果具有相同密钥的实体已存在,EF 将在查询结果中包括该密钥。 尽管 EF 仍将针对数据库发出查询,但此行为可以绕过实体多次实现的大部分成本。

3.1.1 使用 DbContext 查找从对象缓存获取实体3.1.1 Getting entities from the object cache using DbContext Find

与常规查询不同,DbSet 中的 Find 方法(在 EF 4.1 中首次包含 API)将在内存中执行搜索,甚至针对数据库发出查询。 请务必注意,两个不同的 ObjectObjectContext 实例将具有两个不同的 ObjectStateManager 实例,这意味着它们具有单独的对象缓存。

Find 使用主键值尝试查找由上下文跟踪的实体。 如果实体不在上下文中,则将针对数据库执行和评估查询,如果在上下文中或数据库中找不到该实体,则返回 null。 请注意,Find 还会返回已添加到上下文但尚未保存到数据库的实体。

使用 Find 时需要考虑性能。 默认情况下,调用此方法将触发对象缓存的验证,以检测仍挂起的提交到数据库的更改。 如果对象缓存或大型对象图形中有大量对象添加到对象缓存中,此过程可能非常昂贵,但也可以禁用。 在某些情况下,在禁用自动检测更改时,在调用 Find 方法时,您可能会感觉到在调用 Find 方法时,差异程度超过一个数量级。 但是,当对象实际位于缓存中时,以及必须从数据库中检索对象时,会感知到第二个数量级。 下面是一个示例图,使用我们的一些微基准(以毫秒表示)进行测量,负载为 5000 个实体:

.NET 4.5 对数刻度

禁用自动检测更改的查找示例:

  1. context.Configuration.AutoDetectChangesEnabled = false;
  2. var product = context.Products.Find(productId);
  3. context.Configuration.AutoDetectChangesEnabled = true;
  4. ...

使用 Find 方法时必须考虑的是:

  1. 如果对象不在缓存中,则 Find 的好处将被否定,但语法仍比按键查询更简单。
  2. 如果启用自动检测更改,Find 方法的成本可能会增加一个数量级,甚至更多取决于模型的复杂性和对象缓存中的实体数量。

此外,请记住,Find 仅返回要查找的实体,如果关联实体尚未在对象缓存中,则不会自动加载它们。 如果需要检索关联的实体,可以使用按键进行查询,并进行热切加载。 有关详细信息,请参阅8.1 延迟加载与热加载

3.1.2 当对象缓存具有多个实体时的性能问题3.1.2 Performance issues when the object cache has many entities

对象缓存有助于提高实体框架的总体响应能力。 但是,当对象缓存加载的实体量非常大时,可能会影响某些操作,如添加、删除、查找、输入、保存更改等。 特别是,触发调用检测更改的操作将受到非常大的对象缓存的负面影响。 检测更改将对象图形与对象状态管理器同步,其性能将直接由对象图形的大小决定。 有关检测更改的详细信息,请参阅跟踪POCO 实体中的更改

使用实体框架 6 时,开发人员可以直接在 DbSet 上调用 AddRange 和 RemoveRange,而不是在集合上迭代,并按实例调用一次 Add。 使用范围方法的优点是,检测更改的成本仅针对整个实体集支付一次,而不是每个添加的实体支付一次。

3.2 查询计划缓存3.2 Query Plan Caching

第一次执行查询时,它会通过内部计划编译器将概念查询转换为存储命令(例如,在对 SQL Server 运行时执行的 T-SQL)。如果启用了查询计划缓存,则下次执行查询时,将直接从查询计划缓存中检索存储命令以执行,绕过计划编译器。

查询计划缓存在同一 AppDomain 中的 ObjectContext 实例之间共享。 您无需保留 ObjectContext 实例即可从查询计划缓存中获益。

3.2.1 关于查询计划缓存的一些注释3.2.1 Some notes about Query Plan Caching

  • 查询计划缓存为所有查询类型共享:实体 SQL、LINQ 到实体和编译查询对象。
  • 默认情况下,为实体 SQL 查询启用查询计划缓存,无论是通过实体命令还是通过对象查询执行。 默认情况下,在 .NET 4.5 上的实体框架中,在实体框架中,在实体框架中,LINQ 对实体查询也启用了它,
    • 通过将启用计划缓存属性(在实体命令或对象查询上)设置为 false,可以禁用查询计划缓存。 例如:
  1. var query = from customer in context.Customer
  2. where customer.CustomerId == id
  3. select new
  4. {
  5. customer.CustomerId,
  6. customer.Name
  7. };
  8. ObjectQuery oQuery = query as ObjectQuery;
  9. oQuery.EnablePlanCaching = false;
  • 对于参数化查询,更改参数的值仍将命中缓存的查询。 但是,更改参数的分面(例如,大小、精度或比例)将击中缓存中的不同条目。
  • 使用实体 SQL 时,查询字符串是密钥的一部分。 更改查询将会导致不同的缓存条目,即使查询在功能上等效。 这包括对套管或空白的更改。
  • 使用 LINQ 时,将处理查询以生成密钥的一部分。 因此,更改 LINQ 表达式将生成不同的键。
  • 可能适用其他技术限制;有关详细信息,请参阅自动编译查询。

3.2.2 缓存逐出算法3.2.2 Cache eviction algorithm

了解内部算法的工作原理将帮助您确定何时启用或禁用查询计划缓存。 清理算法如下:

  1. 一旦缓存包含一组条目数 (800),我们将启动一个计时器,定期(每分钟一次)扫描缓存。
  2. 在缓存扫描期间,条目会以 LFRU(最不频繁和最近使用)为基础从缓存中删除。 此算法在决定弹出哪些条目时,会考虑命中计数和年龄。
  3. 在每个缓存扫描结束时,缓存再次包含 800 个条目。

在确定要驱逐的条目时,所有缓存条目都一视同仁。 这意味着编译查询的存储命令与实体 SQL 查询的存储命令具有相同的逐出机会。

请注意,缓存驱逐计时器在缓存中有 800 个实体时被踢中,但缓存仅在启动此计时器 60 秒后被扫描。 这意味着,最多 60 秒,您的缓存可能会增长到相当大。

3.2.3 演示查询计划缓存性能的测试指标3.2.3 Test Metrics demonstrating query plan caching performance

为了演示查询计划缓存对应用程序性能的影响,我们执行了一个测试,其中针对 Navision 模型执行了许多实体 SQL 查询。 有关 Navision 模型的说明以及已执行的查询类型,请参阅附录。 在此测试中,我们首先迭代查询列表,并执行每个查询一次以将其添加到缓存(如果启用了缓存)。 此步骤是非时无刻的。 接下来,我们将主线程休眠超过 60 秒,以便进行缓存扫描;最后,我们第二次遍通列表以执行缓存的查询。 此外,在执行每组查询之前刷新 SQL Server 计划缓存,以便我们获取的时间准确反映查询计划缓存提供的好处。

3.2.3.1 测试结果3.2.3.1 Test Results
测试EF5 无缓存EF5 缓存EF6 无缓存EF6 缓存
枚举所有 18723 个查询124125.4124.3125.3
避免扫描(仅前 800 个查询,无论复杂性如何)41.75.540.55.4
仅聚合子总计查询(总计 178 个 - 避免扫描)39.54.538.14.6

所有时间(以秒为单位)。

道德 - 在执行大量不同的查询(例如,动态创建的查询)时,缓存不起作用,并且生成的缓存刷新可以使从计划缓存中从实际使用中受益最大的查询保持。

聚合子总计查询是测试的查询中最复杂的。 正如预期的那样,查询越复杂,您从查询计划缓存中看到的好处就越大。

由于编译查询实际上是一个 LINQ 查询,其计划缓存,因此编译查询与等效实体 SQL 查询的比较应具有类似的结果。 事实上,如果应用具有大量动态实体 SQL 查询,则使用查询填充缓存也会有效地导致编译查询在从缓存中刷新时”去编译”。 在这种情况下,可以通过禁用动态查询的缓存来确定编译查询的优先级,从而提高性能。 当然,更好的是重写应用以使用参数化查询而不是动态查询。

3.3 使用编译查询提高 LINQ 查询的性能3.3 Using CompiledQuery to improve performance with LINQ queries

我们的测试表明,使用编译查询可以带来 7% 的好处比自动编译的 LINQ 查询;这意味着您将在实体框架堆栈中执行代码的时间减少 7%;这并不意味着您的应用程序将加快 7%。 一般来说,在 EF 5.0 中写入和维护编译查询对象的成本与收益相比可能不值得麻烦。 您的里程可能会有所不同,因此,如果您的项目需要额外的推送,请执行此选项。 请注意,编译查询仅与对象上下文派生模型兼容,并且与 DbContext 派生的模型不兼容。

有关创建和调用编译查询的详细信息,请参阅编译查询(LINQ 到实体)。

使用 Compilequery 时,您需要考虑两个注意事项,即使用静态实例的要求及其可组合性问题。 下面对这两个注意事项进行了深入解释。

3.3.1 使用静态编译查询实例3.3.1 Use static CompiledQuery instances

由于编译 LINQ 查询是一个耗时的过程,因此我们不希望每次需要从数据库获取数据时都这样做。 编译查询实例允许您编译一次并运行多次,但您必须小心并采购,以便每次都重复使用同一个编译查询实例,而不是反复编译它。 使用静态成员来存储编译查询实例是必要的;否则,您不会看到任何好处。

例如,假设您的页面具有以下方法正文来处理显示所选类别的产品:

  1. // Warning: this is the wrong way of using CompiledQuery
  2. using (NorthwindEntities context = new NorthwindEntities())
  3. {
  4. string selectedCategory = this.categoriesList.SelectedValue;
  5. var productsForCategory = CompiledQuery.Compile<NorthwindEntities, string, IQueryable<Product>>(
  6. (NorthwindEntities nwnd, string category) =>
  7. nwnd.Products.Where(p => p.Category.CategoryName == category)
  8. );
  9. this.productsGrid.DataSource = productsForCategory.Invoke(context, selectedCategory).ToList();
  10. this.productsGrid.DataBind();
  11. }
  12. this.productsGrid.Visible = true;

在这种情况下,每次调用方法时,您都会动态创建新的编译查询实例。 CompiledQuery 不会通过从查询计划缓存中检索存储命令来看到性能优势,而是每次创建新实例时都会通过计划编译器。 事实上,每次调用该方法时,您都会使用新的编译查询条目来污染查询计划缓存。

相反,您希望创建已编译查询的静态实例,以便每次调用方法时都调用相同的已编译查询。 这样做的一种方法是添加编译查询实例作为对象上下文的成员。然后,您可以通过帮助器方法访问编译查询,使事情变得更干净一些:

  1. public partial class NorthwindEntities : ObjectContext
  2. {
  3. private static readonly Func<NorthwindEntities, string, IEnumerable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
  4. (NorthwindEntities context, string categoryName) =>
  5. context.Products.Where(p => p.Category.CategoryName == categoryName)
  6. );
  7. public IEnumerable<Product> GetProductsForCategory(string categoryName)
  8. {
  9. return productsForCategoryCQ.Invoke(this, categoryName).ToList();
  10. }

此帮助器方法将调用如下:

  1. this.productsGrid.DataSource = context.GetProductsForCategory(selectedCategory);

3.3.2 通过编译查询进行组合3.3.2 Composing over a CompiledQuery

通过任何 LINQ 查询进行撰写的能力非常有用;为此,只需在IQuery()Count() 之后调用方法。 但是,这样做实质上会返回一个新的可查询对象。 虽然在技术上没有什么可以阻止您通过 CompiledQuery 进行创作,但这样做将导致生成需要再次通过计划编译器的新 IQuery 对象。

某些组件将使用组合的 IQuery 对象来实现高级功能。 例如,ASP。NET 的 GridView 可以通过 SelectMethod 属性将数据绑定到 IQuery 对象。 然后,GridView 将在此可查询对象上进行组合,以允许对数据模型进行排序和分页。 如您所见,使用 GridView 的编译查询不会命中已编译的查询,而是生成新的自动编译查询。

在一个可能遇到这种情况的地方是向查询添加累进筛选器时。 例如,假设您有一个客户页面,该页面包含多个可选筛选器的下拉列表(例如,国家/地区订单和订单计数)。 您可以在编译查询的 IQuery 可查询结果上编写这些筛选器,但这样做将导致每次执行计划编译器时新查询都会经过计划编译器。

  1. using (NorthwindEntities context = new NorthwindEntities())
  2. {
  3. IQueryable<Customer> myCustomers = context.InvokeCustomersForEmployee();
  4. if (this.orderCountFilterList.SelectedItem.Value != defaultFilterText)
  5. {
  6. int orderCount = int.Parse(orderCountFilterList.SelectedValue);
  7. myCustomers = myCustomers.Where(c => c.Orders.Count > orderCount);
  8. }
  9. if (this.countryFilterList.SelectedItem.Value != defaultFilterText)
  10. {
  11. myCustomers = myCustomers.Where(c => c.Address.Country == countryFilterList.SelectedValue);
  12. }
  13. this.customersGrid.DataSource = myCustomers;
  14. this.customersGrid.DataBind();
  15. }

为了避免此重新编译,您可以重写编译查询,以考虑可能的筛选器:

  1. private static readonly Func<NorthwindEntities, int, int?, string, IQueryable<Customer>> customersForEmployeeWithFiltersCQ = CompiledQuery.Compile(
  2. (NorthwindEntities context, int empId, int? countFilter, string countryFilter) =>
  3. context.Customers.Where(c => c.Orders.Any(o => o.EmployeeID == empId))
  4. .Where(c => countFilter.HasValue == false || c.Orders.Count > countFilter)
  5. .Where(c => countryFilter == null || c.Address.Country == countryFilter)
  6. );

将在 UI 中调用,例如:

  1. using (NorthwindEntities context = new NorthwindEntities())
  2. {
  3. int? countFilter = (this.orderCountFilterList.SelectedIndex == 0) ?
  4. (int?)null :
  5. int.Parse(this.orderCountFilterList.SelectedValue);
  6. string countryFilter = (this.countryFilterList.SelectedIndex == 0) ?
  7. null :
  8. this.countryFilterList.SelectedValue;
  9. IQueryable<Customer> myCustomers = context.InvokeCustomersForEmployeeWithFilters(
  10. countFilter, countryFilter);
  11. this.customersGrid.DataSource = myCustomers;
  12. this.customersGrid.DataBind();
  13. }

此处的权衡是生成的存储命令将始终具有带有空检查的筛选器,但对于数据库服务器来说,优化这些筛选器应该相当简单:

  1. ...
  2. WHERE ((0 = (CASE WHEN (@p__linq__1 IS NOT NULL) THEN cast(1 as bit) WHEN (@p__linq__1 IS NULL) THEN cast(0 as bit) END)) OR ([Project3].[C2] > @p__linq__2)) AND (@p__linq__3 IS NULL OR [Project3].[Country] = @p__linq__4)

3.4 元数据缓存3.4 Metadata caching

实体框架还支持元数据缓存。 这实质上是跨不同连接到同一模型的类型信息和类型到数据库映射信息。 元数据缓存对于每个 AppDomain 是唯一的。

3.4.1 元数据缓存算法3.4.1 Metadata Caching algorithm

  1. 模型的元数据信息存储在每个实体连接的项目集合中。

    • 作为旁注,模型的不同部分有不同的 ItemCollection 对象。 例如,StoreItem集合包含有关数据库模型的信息;因此,它包含有关数据库模型的信息。ObjectItemCollection 包含有关数据模型的信息;EdmItemCollection 包含有关概念模型的信息。
  2. 如果两个连接使用相同的连接字符串,它们将共享相同的 ItemCollection 实例。

  3. 功能上等效,但文本上不同的连接字符串可能会导致不同的元数据缓存。 我们使用令牌化连接字符串,因此只需更改令牌的顺序即可生成共享元数据。 但是,在标记化后,两个在功能上看起来相同的连接字符串可能不会被计算为相同。

  4. 定期检查项目集合以表示使用。 如果确定最近未访问工作区,则将在下一次缓存扫描时将其标记为清理。

  5. 仅仅创建实体连接就会导致创建元数据缓存(尽管在打开连接之前不会初始化其中的项集合)。 此工作区将保留在内存中,直到缓存算法确定它不”正在使用”。

客户咨询团队撰写了一篇博客文章,其中描述了对 ItemCollection 的引用,以避免在使用大型模型时”弃用”: < https://docs.microsoft.com/archive/blogs/appfabriccat/holding-a-reference-to-the-ef-metadataworkspace-for-wcf-services>。

3.4.2 元数据缓存和查询计划缓存之间的关系3.4.2 The relationship between Metadata Caching and Query Plan Caching

查询计划缓存实例位于元数据工作区的存储类型的项目集合中。 这意味着缓存存储命令将用于查询使用给定元数据工作区实例化的任何上下文。 这也意味着,如果您有两个连接字符串略有不同,在标记后不匹配,则将具有不同的查询计划缓存实例。

3.5 结果缓存3.5 Results caching

使用结果缓存(也称为”第二级缓存”),您将查询的结果保存在本地缓存中。 发出查询时,首先在查询存储之前,先查看结果是否在本地可用。 虽然实体框架并不直接支持结果缓存,但可以使用包装提供程序添加第二级缓存。 一个带有第二级缓存的包装提供程序的示例是 Alachisoft基于 NCache 的实体框架第二级缓存

二级缓存的实现是在 LINQ 表达式计算(和 fee)并从第一级缓存中计算或检索查询执行计划之后发生的注入功能。 然后,第二级缓存将仅存储原始数据库结果,因此具体化管道在之后仍执行。

3.5.1 使用包装提供程序进行结果缓存的其他引用3.5.1 Additional references for results caching with the wrapping provider

4 自动编译查询4 Autocompiled Queries

使用实体框架对数据库发出查询时,它必须经过一系列步骤才能实际实现结果;其中一个步骤是查询编译。 实体 SQL 查询在自动缓存时具有良好的性能,因此,在进行同一查询的第二次或第三次时,它可以跳过计划编译器并使用缓存的计划。

实体框架 5 还引入了 LINQ 到实体查询的自动缓存。 在以往版本的实体框架中,创建编译查询以加快性能是一种常见做法,因为这将使 LINQ 到实体查询可缓存。 由于缓存现在无需使用编译查询即可自动完成,因此我们将此功能称为”自动编译查询”。 有关查询计划缓存及其机制的详细信息,请参阅查询计划缓存。

实体框架检测查询何时需要重新编译,并且在调用查询时(即使以前已编译)也执行。 导致重新编译查询的常见条件是:

  • 更改与您的查询关联的合并选项。 将不会使用缓存的查询,而是计划编译器将再次运行,并缓存新创建的计划。
  • 更改上下文选项的值。 您将获得与更改合并选项相同的效果。

其他条件可能会阻止查询使用缓存。 常见示例包括:

  • 使用 IE500T<>。包含<>(T 值)。
  • 使用生成具有常量的查询的函数。
  • 使用非映射对象的属性。
  • 将查询链接到需要重新编译的另一个查询。

4.1 使用 IE500T<。>包含值)4.1 Using IEnumerable.Contains(T value)

实体框架不缓存调用 IE55T<的>查询。包含<针对内存>中集合的 T(T 值),因为集合的值被视为易失性。 以下示例查询将不会缓存,因此计划编译器将始终对其进行处理:

  1. int[] ids = new int[10000];
  2. ...
  3. using (var context = new MyContext())
  4. {
  5. var query = context.MyEntities
  6. .Where(entity => ids.Contains(entity.Id));
  7. var results = query.ToList();
  8. ...
  9. }

请注意,执行”包含”的 IE55 的大小的确定查询的编译速度或速度。 使用大型集合(如上例中所示的集合)时,性能可能会受到显著影响。

实体框架 6 包含对 IE50t方式的优化。在执行查询<时>,包含 T(T 值)工作。 生成的 SQL 代码生成速度要快得多,而且可读性更强,在大多数情况下,它还在服务器中执行得更快。

4.2 使用生成具有常量的查询的函数4.2 Using functions that produce queries with constants

Skip()、Take()、包含()和 DefautIfEmpty() LINQ 运算符不生成具有参数的 SQL 查询,而是将传递给它们的值作为常量传递给它们。 因此,否则可能相同的查询最终会污染 EF 堆栈和数据库服务器上的查询计划缓存,除非在后续查询执行中使用相同的常量,否则不会重新使用。 例如:

  1. var id = 10;
  2. ...
  3. using (var context = new MyContext())
  4. {
  5. var query = context.MyEntities.Select(entity => entity.Id).Contains(id);
  6. var results = query.ToList();
  7. ...
  8. }

在此示例中,每次执行此查询时,使用 ID 的不同值,查询将编译为新计划。

在进行分页时,尤其要注意使用跳过和取。 在 EF6 中,这些方法具有 lambda 重载,可有效地使缓存的查询计划可重用,因为 EF 可以捕获传递给这些方法的变量并将其转换为 SQL 参数。 这也有助于保持缓存更干净,否则每个查询使用不同的常量为 Skip 和 Take 将获得其自己的查询计划缓存条目。

请考虑以下代码,该代码不理想,但仅用于举例说明此类查询:

  1. var customers = context.Customers.OrderBy(c => c.LastName);
  2. for (var i = 0; i < count; ++i)
  3. {
  4. var currentCustomer = customers.Skip(i).FirstOrDefault();
  5. ProcessCustomer(currentCustomer);
  6. }

此代码的更快版本将涉及调用使用 lambda 的跳过:

  1. var customers = context.Customers.OrderBy(c => c.LastName);
  2. for (var i = 0; i < count; ++i)
  3. {
  4. var currentCustomer = customers.Skip(() => i).FirstOrDefault();
  5. ProcessCustomer(currentCustomer);
  6. }

第二个代码段的运行速度可能加快 11%,因为每次运行查询时都使用相同的查询计划,从而节省了 CPU 时间并避免污染查询缓存。 此外,由于 Skip 的参数位于闭包中,因此代码现在可能如下所示:

  1. var i = 0;
  2. var skippyCustomers = context.Customers.OrderBy(c => c.LastName).Skip(() => i);
  3. for (; i < count; ++i)
  4. {
  5. var currentCustomer = skippyCustomers.FirstOrDefault();
  6. ProcessCustomer(currentCustomer);
  7. }

4.3 使用非映射对象的属性4.3 Using the properties of a non-mapped object

当查询使用非映射对象类型的属性作为参数时,查询将不会被缓存。 例如:

  1. using (var context = new MyContext())
  2. {
  3. var myObject = new NonMappedType();
  4. var query = from entity in context.MyEntities
  5. where entity.Name.StartsWith(myObject.MyProperty)
  6. select entity;
  7. var results = query.ToList();
  8. ...
  9. }

在此示例中,假设类非映射类型不是实体模型的一部分。 可以轻松地更改此查询,以不使用非映射类型,而是使用本地变量作为查询的参数:

  1. using (var context = new MyContext())
  2. {
  3. var myObject = new NonMappedType();
  4. var myValue = myObject.MyProperty;
  5. var query = from entity in context.MyEntities
  6. where entity.Name.StartsWith(myValue)
  7. select entity;
  8. var results = query.ToList();
  9. ...
  10. }

在这种情况下,查询将能够获得缓存,并从查询计划缓存中获益。

4.4 链接到需要重新编译的查询4.4 Linking to queries that require recompiling

按照上述相同的示例,如果您有第二个依赖于需要重新编译的查询,则整个第二个查询也将重新编译。 下面是一个示例来说明此方案:

  1. int[] ids = new int[10000];
  2. ...
  3. using (var context = new MyContext())
  4. {
  5. var firstQuery = from entity in context.MyEntities
  6. where ids.Contains(entity.Id)
  7. select entity;
  8. var secondQuery = from entity in context.MyEntities
  9. where firstQuery.Any(otherEntity => otherEntity.Id == entity.Id)
  10. select entity;
  11. var results = secondQuery.ToList();
  12. ...
  13. }

该示例是泛型的,但它说明了链接到第一查询如何导致第二查询无法缓存。 如果第一查询不是需要重新编译的查询,则第二查询将被缓存。

5 无跟踪查询5 NoTracking Queries

5.1 禁用更改跟踪以减少状态管理开销5.1 Disabling change tracking to reduce state management overhead

如果您处于只读方案,并且希望避免将对象加载到 ObjectStateManager 中的开销,则可以发出”无跟踪”查询。可以在查询级别禁用更改跟踪。

但请注意,通过禁用更改跟踪,您实际上正在关闭对象缓存。 当您查询实体时,我们不能通过从 ObjectStateManager 提取以前具体化的查询结果来跳过具体化。 如果在同一上下文中反复查询相同的实体,则实际上可能会看到启用更改跟踪的性能优势。

使用 ObjectContext 查询时,ObjectQuery 和 ObjectSet 实例将在设置合并选项后记住它,并且在它们上组成的查询将继承父查询的有效 MergeOption。 使用 DbContext 时,可以通过在 DbSet 上调用 AsNo 跟踪() 修改器来禁用跟踪。

5.1.1 禁用使用 DbContext 时查询的更改跟踪5.1.1 Disabling change tracking for a query when using DbContext

通过将调用链接到查询中的 AsNoTrack() 方法,可以将查询模式切换到 NoTrack。 与对象查询不同,DbContext API 中的 DbSet 和 DbQuery 类没有 MergeOption 的可变属性。

  1. var productsForCategory = from p in context.Products.AsNoTracking()
  2. where p.Category.CategoryName == selectedCategory
  3. select p;

5.1.2 使用 ObjectContext 禁用查询级别的更改跟踪5.1.2 Disabling change tracking at the query level using ObjectContext

  1. var productsForCategory = from p in context.Products
  2. where p.Category.CategoryName == selectedCategory
  3. select p;
  4. ((ObjectQuery)productsForCategory).MergeOption = MergeOption.NoTracking;

5.1.3 使用 ObjectContext 禁用整个实体集的更改跟踪5.1.3 Disabling change tracking for an entire entity set using ObjectContext

  1. context.Products.MergeOption = MergeOption.NoTracking;
  2. var productsForCategory = from p in context.Products
  3. where p.Category.CategoryName == selectedCategory
  4. select p;

5.2 测试指标,演示 NoTrack 查询的性能优势5.2 Test Metrics demonstrating the performance benefit of NoTracking queries

在此测试中,我们将通过将跟踪与 Navision 模型的 NoTrack 查询进行比较来查看填充 ObjectStateManager 的成本。 有关 Navision 模型的说明以及已执行的查询类型,请参阅附录。 在此测试中,我们迭代查询列表并执行每个查询一次。 我们运行了两个测试变体,一次使用 NoTrack 查询,另一次使用默认合并选项”仅追加”。 我们运行每个变体 3 次,并获取运行的平均值。 在测试之间,我们清除 SQL Server 上的查询缓存,并通过运行以下命令来收缩 tempdb:

  1. DBCC DROPCLEANBUFFERS
  2. DBCC FREEPROCCACHE
  3. DBCC SHRINK 数据库(tempdb, 0)

测试结果,3 次以上的中值:

无跟踪 + 工作集无跟踪 + 时间仅追加 = 工作集仅追加 = 时间
实体框架 54603617281163536 毫秒5965455361273042 毫秒
Entity Framework 6647127040190228 毫秒832798720195521 毫秒

实体框架 5 在运行结束时的内存占用量将比实体框架 6 少。 实体框架 6 使用的额外内存是启用新功能和更好性能的额外内存结构和代码的结果。

使用 ObjectStateManager 时,内存占用空间也有明显差异。 实体框架 5 在跟踪我们从数据库中实现的所有实体时,其占用空间增加了 30%。 实体框架 6 这样做时占用空间增加了 28%。

在时间方面,实体框架 6 在此测试中以较大优势优于实体框架 5。 实体框架 6 在实体框架 5 消耗的时间大约 16% 的时间内完成了测试。 此外,使用 ObjectStateManager 时,实体框架 5 需要 9% 的时间才能完成。 相比之下,实体框架 6 在使用 ObjectStateManager 时使用的时间要长 3%。

6 查询执行选项6 Query Execution Options

实体框架提供了几种不同的查询方法。 我们将介绍以下选项,比较每个选项的优缺点,并检查它们的性能特征:

  • 林Q 到实体。
  • 没有跟踪 LINQ 到实体。
  • 对象查询的实体 SQL。
  • 实体命令的实体 SQL。
  • 执行存储查询。
  • SqlQuery。
  • 编译查询。

6.1 LINQ 到实体查询6.1 LINQ to Entities queries

  1. var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");

优点

  • 适用于 CUD 操作。
  • 完全具体化的对象。
  • 最简单的编写语法内置于编程语言中。
  • 性能好。

缺点

  • 某些技术限制,例如:
    • 对外部 JOIN 查询使用默认IfEmpty的模式会导致比实体 SQL 中简单的外部 JOIN 语句更复杂的查询。
    • 您仍然不能将 LIKE 用于常规模式匹配。

6.2 没有跟踪 LINQ 到实体查询6.2 No Tracking LINQ to Entities queries

当上下文派生对象上下文时:

  1. context.Products.MergeOption = MergeOption.NoTracking;
  2. var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");

当上下文派生 DbContext 时:

  1. var q = context.Products.AsNoTracking()
  2. .Where(p => p.Category.CategoryName == "Beverages");

优点

  • 与常规 LINQ 查询性能提高。
  • 完全具体化的对象。
  • 最简单的编写语法内置于编程语言中。

缺点

  • 不适合 CUD 操作。
  • 某些技术限制,例如:
    • 对外部 JOIN 查询使用默认IfEmpty的模式会导致比实体 SQL 中简单的外部 JOIN 语句更复杂的查询。
    • 您仍然不能将 LIKE 用于常规模式匹配。

请注意,即使未指定 NoTracking,也不会跟踪项目标量属性的查询。 例如:

  1. var q = context.Products.Where(p => p.Category.CategoryName == "Beverages").Select(p => new { p.ProductName });

此特定查询未显式指定为 NoTracking,但由于它未具体化对象状态管理器已知的类型,因此不会跟踪具体化结果。

6.3 对象查询的实体 SQL6.3 Entity SQL over an ObjectQuery

  1. ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");

优点

  • 适用于 CUD 操作。
  • 完全具体化的对象。
  • 支持查询计划缓存。

缺点

  • 涉及文本查询字符串,与内置于语言中的查询构造相比,这些字符串更容易出现用户错误。

6.4 实体命令的实体 SQL6.4 Entity SQL over an Entity Command

  1. EntityCommand cmd = eConn.CreateCommand();
  2. cmd.CommandText = "Select p From NorthwindEntities.Products As p Where p.Category.CategoryName = 'Beverages'";
  3. using (EntityDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
  4. {
  5. while (reader.Read())
  6. {
  7. // manually 'materialize' the product
  8. }
  9. }

优点

  • 支持 .NET 4.0 中的查询计划缓存(.NET 4.5 中的所有其他查询类型都支持计划缓存)。

缺点

  • 涉及文本查询字符串,与内置于语言中的查询构造相比,这些字符串更容易出现用户错误。
  • 不适合 CUD 操作。
  • 结果不会自动具体化,必须从数据读取器读取。

6.5 SqlQuery 和执行存储查询6.5 SqlQuery and ExecuteStoreQuery

数据库上的 SqlQuery:

  1. // use this to obtain entities and not track them
  2. var q1 = context.Database.SqlQuery<Product>("select * from products");

DbSet 上的 SqlQuery:

  1. // use this to obtain entities and have them tracked
  2. var q2 = context.Products.SqlQuery("select * from products");

ExecyteStore查询:

  1. var beverages = context.ExecuteStoreQuery<Product>(
  2. @" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued, P.DiscontinuedDate
  3. FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
  4. WHERE (C.CategoryName = 'Beverages')"
  5. );

优点

  • 通常性能最快,因为计划编译器是绕过的。
  • 完全具体化的对象。
  • 适用于从 DbSet 使用的 CUD 操作。

缺点

  • 查询是文本和容易出错的。
  • 通过使用存储语义而不是概念语义,查询绑定到特定的后端。
  • 存在继承时,手工制作的查询需要考虑所请求类型的映射条件。

6.6 编译查询6.6 CompiledQuery

  1. private static readonly Func<NorthwindEntities, string, IQueryable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
  2. (NorthwindEntities context, string categoryName) =>
  3. context.Products.Where(p => p.Category.CategoryName == categoryName)
  4. );
  5. var q = context.InvokeProductsForCategoryCQ("Beverages");

优点

  • 与常规 LINQ 查询(LINQ 查询)提供高达 7% 的性能改进。
  • 完全具体化的对象。
  • 适用于 CUD 操作。

缺点

  • 增加了复杂性和编程开销。
  • 在编译的查询上作曲时,性能改进将丢失。
  • 某些 LINQ 查询不能编写为编译查询 - 例如,匿名类型的投影。

6.7 不同查询选项的性能比较6.7 Performance Comparison of different query options

在未进行时间创建时的简单微观基准测试已过。 我们在受控环境中对一组非缓存实体进行了 5000 次查询。 这些数字应该带有警告:它们并不反映应用程序产生的实际数字,而是非常准确地测量了在比较 Apple 到 apple 的不同查询选项时存在的性能差异量,不包括创建新上下文的成本。

EF测试时间(毫秒)内存
EF5对象上下文 ESQL241438801408
EF5对象上下文林克查询269238277120
EF5数据库上下文林克查询无跟踪281841840640
EF5数据库上下文林克查询293041771008
EF5对象上下文林q查询无跟踪301338412288
EF6对象上下文 ESQL205946039040
EF6对象上下文林克查询307445248512
EF6数据库上下文林克查询无跟踪312547575040
EF6数据库上下文林克查询342047652864
EF6对象上下文林q查询无跟踪359345260800

EF5 微型基准,5000 个暖迭代

EF6 微型基准,5000 个暖迭代

微基准对代码中的小更改非常敏感。 在这种情况下,实体框架 5 和实体框架 6 的成本之间的差额是由于增加了拦截事务性改进。 然而,这些微观基准数字是实体框架所做操作的一个很小的碎片中放大的视野。 从实体框架 5 升级到实体框架 6 时,暖查询的实际方案不应看到性能回归。

为了比较不同查询选项的实际性能,我们创建了 5 个单独的测试变体,其中我们使用不同的查询选项来选择类别名称为”饮料”的所有产品。 每个迭代包括创建上下文的成本,以及实现所有返回的实体的成本。 在采用 1000 个有时算迭代的总和之前,将不及时运行 10 个迭代。 显示的结果是从每次测试的 5 次运行中获取的中位运行。 有关详细信息,请参阅附录 B,其中包含测试的代码。

EF测试时间(毫秒)内存
EF5对象上下文实体命令62139350272
EF5数据库上的 DbContext Sql 查询82537519360
EF5对象上下文存储查询87839460864
EF5对象上下文林q查询无跟踪96938293504
EF5使用对象查询的对象上下文实体 Sql108938981632
EF5对象上下文编译查询109938682624
EF5对象上下文林克查询115238178816
EF5数据库上下文林克查询无跟踪120841803776
EF5DbSet 上的数据库上下文 Sql 查询141437982208
EF5数据库上下文林克查询157441738240
EF6对象上下文实体命令48047247360
EF6对象上下文存储查询49346739456
EF6数据库上的 DbContext Sql 查询61441607168
EF6对象上下文林q查询无跟踪68446333952
EF6使用对象查询的对象上下文实体 Sql76748865280
EF6对象上下文编译查询78848467968
EF6数据库上下文林克查询无跟踪87847554560
EF6对象上下文林克查询95347632384
EF6DbSet 上的数据库上下文 Sql 查询102341992192
EF6数据库上下文林克查询129047529984

EF5 暖查询 1000 次迭代

EF6 暖查询 1000 次迭代

备注

为完整,我们包括一个变体,其中我们在实体命令上执行实体 SQL 查询。 但是,由于此类查询的结果未具体化,因此比较不一定是苹果到苹果。 测试包括接近具体化,试图使比较更公平。

在此端到端案例中,实体框架 6 优于实体框架 5,因为对堆栈的几个部分进行了性能改进,包括更轻的 DbContext 初始化和更快的元数据收集查找。

7 设计时间性能注意事项7 Design time performance considerations

7.1 继承策略7.1 Inheritance Strategies

使用实体框架时的另一个性能考虑是您使用的继承策略。 实体框架支持 3 种基本类型的继承及其组合:

  • 每个层次结构的表 (TPH) - 其中每个继承集映射到具有区分列的表,以指示在行中表示层次结构中的特定类型。
  • 每个类型 (TPT) 的表 - 其中每种类型在数据库中都有自己的表;子表仅定义父表不包含的列。
  • 每个类的表 (TPC) - 其中每种类型的在数据库中都有自己的完整表;子表定义其所有字段,包括父类型中定义的字段。

如果模型使用 TPT 继承,则生成的查询将比其他继承策略生成的查询复杂,这可能导致存储上的执行时间较长。通过 TPT 模型生成查询并实现结果对象通常需要更长时间。

请参阅”在实体框架中使用 TPT(按类型表)继承时的性能注意事项”MSDN 博客文章: < https://docs.microsoft.com/archive/blogs/adonet/performance-considerations-when-using-tpt-table-per-type-inheritance-in-the-entity-framework>。

7.1.1 避免在模型优先或代码优先应用程序中使用 TPT7.1.1 Avoiding TPT in Model First or Code First applications

当您在具有 TPT 架构的现有数据库上创建模型时,您没有很多选项。 但是,在使用 Model First 或代码优先创建应用程序时,出于性能问题,应避免 TPT 继承。

在实体设计器向导中使用模型优先时,您将获得模型中任何继承的 TPT。 如果要使用 Model First 切换到 TPH 继承策略,可以使用 Visual Studio 库 (中的http://visualstudiogallery.msdn.microsoft.com/df3541c3-d833-4b65-b942-989e7ec74c87/)实体设计器数据库生成电源包)中的”实体设计器数据库生成电源包”。

当使用 Code First 配置具有继承的模型映射时,EF 默认将使用 TPH,因此继承层次结构中的所有实体都将映射到同一表。 有关详细信息,请参阅 MSDN 杂志 ()http://msdn.microsoft.com/magazine/hh126815.aspx中”实体框架4.1中代码优先”一文中的”使用流畅 API 映射”部分。

7.2 从 EF4 升级以提高模型生成时间7.2 Upgrading from EF4 to improve model generation time

生成模型存储层 (SSDL) 的算法的 SQL Server 特定改进可在实体框架 5 和 6 中提供,并在安装 Visual Studio 2010 SP1 时作为实体框架 4 的更新提供。 以下测试结果演示了生成非常大的模型时的改进,在这种情况下,Navision 模型。 有关附录 C 的更多详细信息,请参阅附录 C。

该模型包含 1005 个实体集和 4227 个关联集。

配置所消耗的时间细目
视觉工作室 2010, 实体框架 4SSDL 生成: 2 小时 27 分钟
映射生成:1 秒
CSDL 生成:1 秒
对象层生成:1 秒
视图生成: 2 小时 14 分钟
可视化工作室 2010 SP1, 实体框架 4SSDL 生成:1 秒
映射生成:1 秒
CSDL 生成:1 秒
对象层生成:1 秒
视图生成: 1 小时 53 分钟
视觉工作室 2013, 实体框架 5SSDL 生成:1 秒
映射生成:1 秒
CSDL 生成:1 秒
对象层生成:1 秒
视图生成: 65 分钟
视觉工作室 2013, 实体框架 6SSDL 生成:1 秒
映射生成:1 秒
CSDL 生成:1 秒
对象层生成:1 秒
视图生成:28 秒。

值得注意的是,在生成 SSDL 时,负载几乎完全花费在 SQL Server 上,而客户端开发计算机正在等待从服务器返回的结果。 DBA 应特别赞赏这一改进。 还值得注意的是,模型生成的全部成本基本上都发生在视图生成中。

7.3 使用数据库优先和模型第一拆分大型模型7.3 Splitting Large Models with Database First and Model First

随着模型尺寸的增加,设计器表面变得杂乱无章且难以使用。 我们通常认为具有 300 多个实体的模型太大,无法有效地使用设计器。 以下博客文章描述了拆分大型模型的几个选项: < https://docs.microsoft.com/archive/blogs/adonet/working-with-large-models-in-entity-framework-part-2>。

该帖子是为实体框架的第一个版本编写的,但步骤仍然适用。

7.4 实体数据源控制的性能注意事项7.4 Performance considerations with the Entity Data Source Control

我们已经看到多线程性能和压力测试中的情况,其中使用 EntityDataSource 控件的 Web 应用程序的性能显著恶化。 根本原因是 EntityDataSource 反复调用 Web 应用程序引用的程序集上的元数据工作区.LoadFromAssembly,以发现要用作实体的类型。

解决方案是将实体数据源的上下文类型名称设置为派生对象上下文类的类型名称。 这将关闭扫描所有引用的程序集的实体类型的机制。

设置 ContextTypeName 字段还可以防止功能问题,即 .NET 4.0 中的实体数据源在无法通过反射从程序集加载类型时引发反射类型加载异常。 此问题已在 .NET 4.5 中修复。

7.5 POCO 实体和更改跟踪代理7.5 POCO entities and change tracking proxies

实体框架使您能够将自定义数据类与数据模型一起使用,而无需对数据类本身进行任何修改。 这意味着可以将“纯旧式”CLR 对象 (POCO)(例如,现有的域对象)与数据模型一起使用。 这些 POCO 数据类(也称为持久性无知对象)映射到数据模型中定义的实体,支持大多数相同的查询,插入、更新和删除实体数据模型工具生成的实体类型的行为。

实体框架还可以创建从 POCO 类型派生的代理类,当您希望启用诸如 POCO 实体上的延迟加载和自动更改跟踪等功能时,将使用这些类。 您的 POCO 类必须满足某些要求,才能允许实体框架使用代理,如下所述: http://msdn.microsoft.com/library/dd468057.aspx

每次实体的任何属性发生更改时,机会跟踪代理都会通知对象状态管理器,因此实体框架会一直了解实体的实际状态。 这是通过将通知事件添加到属性的 setter 方法的正文,以及让对象状态管理器处理此类事件来实现的。 请注意,由于实体框架创建了一组附加的事件,创建代理实体通常比创建非代理 POCO 实体的成本更高。

当 POCO 实体没有更改跟踪代理时,可以通过将实体的内容与以前保存状态的副本进行比较来找到更改。 当您的上下文中有许多实体时,或者当实体具有非常大的属性时,即使自上次比较以来,这些属性均未更改,这种深层比较将成为一个漫长的过程。

总之:创建更改跟踪代理时,您将支付性能影响,但当实体具有许多属性或模型中有许多实体时,更改跟踪将帮助您加快更改检测过程。 对于具有少量属性的实体,其中实体数量不会增长太多,因此具有更改跟踪代理可能没有多大好处。

8 加载相关实体8 Loading Related Entities

8.1 延迟加载与热负荷8.1 Lazy Loading vs. Eager Loading

实体框架提供了几种加载与目标实体相关的实体的不同方法。 例如,当您查询产品时,相关订单将加载到对象状态管理器中的方式不同。 从性能角度来看,加载相关实体时要考虑的最大问题是是使用延迟加载还是”渴望加载”。

使用”热加载”时,相关实体将随目标实体集一起加载。 在查询中使用”包含”语句来指示要引入哪些相关实体。

使用延迟加载时,初始查询仅引入目标实体集。 但是,每当访问导航属性时,都会针对存储发出另一个查询以加载相关实体。

加载实体后,实体的任何进一步查询都将直接从对象状态管理器加载它,无论您是使用延迟加载还是热要加载。

8.2 如何在延迟加载和渴望加载之间进行选择8.2 How to choose between Lazy Loading and Eager Loading

重要的是,您了解延迟加载和热加载之间的区别,以便您可以为您的应用程序做出正确的选择。 这将有助于评估针对数据库的多个请求与可能包含大型负载的单个请求之间的权衡。 在应用程序的某些部分使用热加载和其他部分的延迟加载可能是合适的。

作为引擎盖下所发生的事情的一个示例,假设您要查询居住在英国的客户及其订单数量。

使用热负荷

  1. using (NorthwindEntities context = new NorthwindEntities())
  2. {
  3. var ukCustomers = context.Customers.Include(c => c.Orders).Where(c => c.Address.Country == "UK");
  4. var chosenCustomer = AskUserToPickCustomer(ukCustomers);
  5. Console.WriteLine("Customer Id: {0} has {1} orders", customer.CustomerID, customer.Orders.Count);
  6. }

使用延迟加载

  1. using (NorthwindEntities context = new NorthwindEntities())
  2. {
  3. context.ContextOptions.LazyLoadingEnabled = true;
  4. //Notice that the Include method call is missing in the query
  5. var ukCustomers = context.Customers.Where(c => c.Address.Country == "UK");
  6. var chosenCustomer = AskUserToPickCustomer(ukCustomers);
  7. Console.WriteLine("Customer Id: {0} has {1} orders", customer.CustomerID, customer.Orders.Count);
  8. }

使用热要加载时,您将发出一个查询,返回所有客户和所有订单。 存储命令如下所示:

  1. SELECT
  2. [Project1].[C1] AS [C1],
  3. [Project1].[CustomerID] AS [CustomerID],
  4. [Project1].[CompanyName] AS [CompanyName],
  5. [Project1].[ContactName] AS [ContactName],
  6. [Project1].[ContactTitle] AS [ContactTitle],
  7. [Project1].[Address] AS [Address],
  8. [Project1].[City] AS [City],
  9. [Project1].[Region] AS [Region],
  10. [Project1].[PostalCode] AS [PostalCode],
  11. [Project1].[Country] AS [Country],
  12. [Project1].[Phone] AS [Phone],
  13. [Project1].[Fax] AS [Fax],
  14. [Project1].[C2] AS [C2],
  15. [Project1].[OrderID] AS [OrderID],
  16. [Project1].[CustomerID1] AS [CustomerID1],
  17. [Project1].[EmployeeID] AS [EmployeeID],
  18. [Project1].[OrderDate] AS [OrderDate],
  19. [Project1].[RequiredDate] AS [RequiredDate],
  20. [Project1].[ShippedDate] AS [ShippedDate],
  21. [Project1].[ShipVia] AS [ShipVia],
  22. [Project1].[Freight] AS [Freight],
  23. [Project1].[ShipName] AS [ShipName],
  24. [Project1].[ShipAddress] AS [ShipAddress],
  25. [Project1].[ShipCity] AS [ShipCity],
  26. [Project1].[ShipRegion] AS [ShipRegion],
  27. [Project1].[ShipPostalCode] AS [ShipPostalCode],
  28. [Project1].[ShipCountry] AS [ShipCountry]
  29. FROM ( SELECT
  30. [Extent1].[CustomerID] AS [CustomerID],
  31. [Extent1].[CompanyName] AS [CompanyName],
  32. [Extent1].[ContactName] AS [ContactName],
  33. [Extent1].[ContactTitle] AS [ContactTitle],
  34. [Extent1].[Address] AS [Address],
  35. [Extent1].[City] AS [City],
  36. [Extent1].[Region] AS [Region],
  37. [Extent1].[PostalCode] AS [PostalCode],
  38. [Extent1].[Country] AS [Country],
  39. [Extent1].[Phone] AS [Phone],
  40. [Extent1].[Fax] AS [Fax],
  41. 1 AS [C1],
  42. [Extent2].[OrderID] AS [OrderID],
  43. [Extent2].[CustomerID] AS [CustomerID1],
  44. [Extent2].[EmployeeID] AS [EmployeeID],
  45. [Extent2].[OrderDate] AS [OrderDate],
  46. [Extent2].[RequiredDate] AS [RequiredDate],
  47. [Extent2].[ShippedDate] AS [ShippedDate],
  48. [Extent2].[ShipVia] AS [ShipVia],
  49. [Extent2].[Freight] AS [Freight],
  50. [Extent2].[ShipName] AS [ShipName],
  51. [Extent2].[ShipAddress] AS [ShipAddress],
  52. [Extent2].[ShipCity] AS [ShipCity],
  53. [Extent2].[ShipRegion] AS [ShipRegion],
  54. [Extent2].[ShipPostalCode] AS [ShipPostalCode],
  55. [Extent2].[ShipCountry] AS [ShipCountry],
  56. CASE WHEN ([Extent2].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
  57. FROM [dbo].[Customers] AS [Extent1]
  58. LEFT OUTER JOIN [dbo].[Orders] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
  59. WHERE N'UK' = [Extent1].[Country]
  60. ) AS [Project1]
  61. ORDER BY [Project1].[CustomerID] ASC, [Project1].[C2] ASC

使用延迟加载时,最初将发出以下查询:

  1. SELECT
  2. [Extent1].[CustomerID] AS [CustomerID],
  3. [Extent1].[CompanyName] AS [CompanyName],
  4. [Extent1].[ContactName] AS [ContactName],
  5. [Extent1].[ContactTitle] AS [ContactTitle],
  6. [Extent1].[Address] AS [Address],
  7. [Extent1].[City] AS [City],
  8. [Extent1].[Region] AS [Region],
  9. [Extent1].[PostalCode] AS [PostalCode],
  10. [Extent1].[Country] AS [Country],
  11. [Extent1].[Phone] AS [Phone],
  12. [Extent1].[Fax] AS [Fax]
  13. FROM [dbo].[Customers] AS [Extent1]
  14. WHERE N'UK' = [Extent1].[Country]

每次访问客户的”订单”导航属性时,都会针对商店发出另一个查询,如下所示:

  1. exec sp_executesql N'SELECT
  2. [Extent1].[OrderID] AS [OrderID],
  3. [Extent1].[CustomerID] AS [CustomerID],
  4. [Extent1].[EmployeeID] AS [EmployeeID],
  5. [Extent1].[OrderDate] AS [OrderDate],
  6. [Extent1].[RequiredDate] AS [RequiredDate],
  7. [Extent1].[ShippedDate] AS [ShippedDate],
  8. [Extent1].[ShipVia] AS [ShipVia],
  9. [Extent1].[Freight] AS [Freight],
  10. [Extent1].[ShipName] AS [ShipName],
  11. [Extent1].[ShipAddress] AS [ShipAddress],
  12. [Extent1].[ShipCity] AS [ShipCity],
  13. [Extent1].[ShipRegion] AS [ShipRegion],
  14. [Extent1].[ShipPostalCode] AS [ShipPostalCode],
  15. [Extent1].[ShipCountry] AS [ShipCountry]
  16. FROM [dbo].[Orders] AS [Extent1]
  17. WHERE [Extent1].[CustomerID] = @EntityKeyValue1',N'@EntityKeyValue1 nchar(5)',@EntityKeyValue1=N'AROUT'

有关详细信息,请参阅加载相关对象

8.2.1 延迟加载与渴望加载备忘单8.2.1 Lazy Loading versus Eager Loading cheat sheet

选择热切装载和延迟装载,没有一刀切的东西。 首先尝试了解两种策略之间的差异,以便您可以做出明智的决策;此外,请考虑您的代码是否适合以下任何方案:

场景我们的建议
是否需要从提取的实体访问许多导航属性?- 两个选项都可能都能做到。 但是,如果查询带来的有效负载不是太大,则使用 Eager 加载可能会获得性能优势,因为它需要较少的网络往返行程来实现对象。

- 如果需要从实体访问许多导航属性,则可以通过在查询中使用多个包含语句来使用”渴望加载”来执行此操作。 包含的实体越多,查询返回的有效负载就越大。 将三个或多个实体包含在查询中后,请考虑切换到延迟加载。
您确切知道运行时需要哪些数据吗?- 延迟加载将更好地为您。 否则,您最终可能会查询不需要的数据。

是的- 渴望加载可能是您最好的选择;这将有助于更快地加载整个集。 如果查询需要获取大量数据,并且速度太慢,请尝试”延迟加载”。
您的代码执行是否远离数据库? (增加网络延迟)- 当网络延迟不是问题时,使用延迟加载可能会简化代码。 请记住,应用程序的拓扑可能会更改,因此不要认为数据库邻近性是理所当然的。

- 当网络出现问题时,只有您才能决定哪种方案更适合您的方案。 通常,热负荷会更好,因为它需要更少的往返行程。

8.2.2 具有多个包括的性能问题8.2.2 Performance concerns with multiple Includes

当我们听到涉及服务器响应时间问题的性能问题时,问题的根源是经常使用多个 Include 语句进行查询。 虽然在查询中包含相关实体很强大,但了解封面下发生的情况非常重要。

包含多个 Include 语句的查询需要相当长的时间才能通过内部计划编译器来生成存储命令。 这一大部分时间都花在尝试优化生成的查询上。 生成的存储命令将包含每个”包含”的外部联接或联合,具体取决于您的映射。 像这样的查询将在单个负载中从数据库引入大型连接图形,从而消除任何带宽问题,尤其是在有效负载中存在大量冗余时(例如,当多个级别的 Include 用于在一对多方向上遍历关联时)。

通过使用 ToTraceString 并在 SQL 服务器管理工作室中执行存储命令以查看有效负载大小,可以检查查询返回过大的有效负载的情况。 在这种情况下,您可以尝试减少查询中的包含语句数,以便仅引入所需的数据。 或者,您可能能够将查询分解为较小的子查询序列,例如:

在断开查询之前:

  1. using (NorthwindEntities context = new NorthwindEntities())
  2. {
  3. var customers = from c in context.Customers.Include(c => c.Orders)
  4. where c.LastName.StartsWith(lastNameParameter)
  5. select c;
  6. foreach (Customer customer in customers)
  7. {
  8. ...
  9. }
  10. }

中断查询后:

  1. using (NorthwindEntities context = new NorthwindEntities())
  2. {
  3. var orders = from o in context.Orders
  4. where o.Customer.LastName.StartsWith(lastNameParameter)
  5. select o;
  6. orders.Load();
  7. var customers = from c in context.Customers
  8. where c.LastName.StartsWith(lastNameParameter)
  9. select c;
  10. foreach (Customer customer in customers)
  11. {
  12. ...
  13. }
  14. }

这仅适用于跟踪的查询,因为我们正在利用上下文自动执行标识解析和关联修复的能力。

与延迟加载一样,权衡将是对较小负载的更多查询。 您还可以使用单个属性的预测来仅显式选择每个实体所需的数据,但在这种情况下,您将不会加载实体,并且不支持更新。

8.2.3 获取延迟加载属性的解决方法8.2.3 Workaround to get lazy loading of properties

实体框架当前不支持延迟加载标量或复杂属性。 但是,如果表包含大型对象(如 BLOB),则可以使用表拆分将大型属性分隔为单独的实体。 例如,假设您有一个包含 varbinary 照片列的产品表。 如果查询中频繁不需要访问此属性,则可以使用表拆分仅引入通常需要的实体部分。 仅当您明确需要产品照片时,才会加载表示产品照片的实体。

吉尔·芬克的”实体框架中的表拆分”博客文章是演示如何启用表拆分的一个很好的资源: < http://blogs.microsoft.co.il/blogs/gilf/archive/2009/10/13/table-splitting-in-entity-framework.aspx>.

9 其他注意事项9 Other considerations

9.1 服务器垃圾回收9.1 Server Garbage Collection

某些用户可能会遇到资源争用,从而限制在垃圾回收器未正确配置时期望的并行性。 每当在多线程方案中使用 EF 时,或任何类似于服务器端系统的应用程序中,请确保启用服务器垃圾回收。 这是通过应用程序配置文件中的简单设置完成的:

  1. <?xmlversion="1.0" encoding="utf-8" ?>
  2. <configuration>
  3. <runtime>
  4. <gcServer enabled="true" />
  5. </runtime>
  6. </configuration>

这将减少线程争用,并在 CPU 饱和方案中将吞吐量提高高达 30%。 通常,您应该始终使用经典垃圾回收(更好地针对 UI 和客户端方案)以及服务器垃圾回收测试应用程序的表现。

9.2 自动检测更改9.2 AutoDetectChanges

如前所述,当对象缓存具有多个实体时,实体框架可能会显示性能问题。 某些操作(如添加、删除、查找、输入和保存更改)会触发对检测更改的调用,这些调用可能会根据对象缓存变得多大而消耗大量 CPU。 原因是对象缓存和对象状态管理器尝试在对上下文执行的每个操作上保持尽可能同步,以便在各种方案下保证生成的数据正确。

通常,最好在应用程序的整个生命周期内启用实体框架的自动更改检测。 如果您的方案受到 CPU 使用率高的负面影响,并且您的配置文件指示罪魁祸首是调用检测更改,请考虑暂时关闭代码敏感部分中的自动检测更改:

  1. try
  2. {
  3. context.Configuration.AutoDetectChangesEnabled = false;
  4. var product = context.Products.Find(productId);
  5. ...
  6. }
  7. finally
  8. {
  9. context.Configuration.AutoDetectChangesEnabled = true;
  10. }

在关闭自动检测更改之前,最好了解这可能会导致实体框架失去跟踪有关实体上发生的更改的某些信息的能力。 如果处理不正确,则可能会导致应用程序上的数据不一致。 有关关闭自动检测更改的详细信息,请阅读http://blog.oneunicorn.com/2012/03/12/secrets-of-detectchanges-part-3-switching-off-automatic-detectchanges/

9.3 每个请求的上下文9.3 Context per request

实体框架的上下文旨在用作短期实例,以便提供最佳的性能体验。 上下文预期是短暂的和丢弃的,因此已经实现为非常轻量级,并尽可能重用元数据。 在 Web 方案中,请务必牢记这一点,并且不应具有超过单个请求持续时间的上下文。 同样,在非 Web 方案中,应根据您对实体框架中不同缓存级别的理解而丢弃上下文。 一般来说,在应用程序的整个生命周期中,应避免使用上下文实例,以及每个线程和静态上下文的上下文。

9.4 数据库无效语义9.4 Database null semantics

默认情况下,实体框架将生成具有 C#空比较语义的 SQL 代码。 请参考以下示例查询:

  1. int? categoryId = 7;
  2. int? supplierId = 8;
  3. decimal? unitPrice = 0;
  4. short? unitsInStock = 100;
  5. short? unitsOnOrder = 20;
  6. short? reorderLevel = null;
  7. var q = from p incontext.Products
  8. wherep.Category.CategoryName == "Beverages"
  9. || (p.CategoryID == categoryId
  10. || p.SupplierID == supplierId
  11. || p.UnitPrice == unitPrice
  12. || p.UnitsInStock == unitsInStock
  13. || p.UnitsOnOrder == unitsOnOrder
  14. || p.ReorderLevel == reorderLevel)
  15. select p;
  16. var r = q.ToList();

在此示例中,我们将许多可空变量与实体上的空属性(如供应商 ID 和 UnitPrice)进行比较。 此查询生成的 SQL 将询问参数值是否与列值相同,或者参数和列值是否为空。 这将隐藏数据库服务器处理空的方式,并将跨不同的数据库供应商提供一致的#C null 体验。 另一方面,生成的代码有点复杂,当查询语句中的比较量增长到大量时,可能无法很好地执行。

处理这种情况的一种方法是使用数据库空语义。 请注意,这可能与 C# null 语义的行为不同,因为现在实体框架将生成更简单的 SQL,公开数据库引擎处理空值的方式。 数据库空语义可以针对上下文配置使用单个配置行激活每个上下文:

  1. context.Configuration.UseDatabaseNullSemantics = true;

使用数据库空语义时,中小型查询不会显示可察觉的性能改进,但在具有大量潜在空比较的查询上,差异将变得更加明显。

在上面的示例查询中,在受控环境中运行的微基准中,性能差异小于 2%。

9.5 异步9.5 Async

实体框架 6 在 .NET 4.5 或更高版本上运行时引入了对异步操作的支持。 在大多数情况下,具有 IO 相关争用的应用程序将从使用异步查询和保存操作中受益最大。 如果应用程序没有受到 IO 争用的影响,则在最好情况下,使用异步将同步运行,并在与同步调用相同的时间内返回结果,或者在最坏的情况下,只需将执行推迟到异步任务,并添加额外的时间来完成您的方案。

有关异步编程的工作原理的信息,这将有助于您确定异步程序是否会提高应用程序访问http://msdn.microsoft.com/library/hh191443.aspx的性能。 有关在实体框架上使用异步操作的详细信息,请参阅异步查询和保存

9.6 NGEN9.6 NGEN

实体框架 6 不在 .NET 框架的默认安装中。 因此,默认情况下,实体框架程序集不是 NGEN d,这意味着所有实体框架代码都受与任何其他 MSIL 程序集相同的 JIT 成本的约束。 这可能会降低 F5 在开发时的经验,也会降低应用程序在生产环境中的冷启动。 为了降低 JIT 的 CPU 和内存成本,建议酌情使用实体框架映像进行 NGEN。 有关如何使用 NGEN 提高实体框架 6 的启动性能的详细信息,请参阅使用 NGen 提高启动性能

9.7 代码优先与 EDMX9.7 Code First versus EDMX

实体框架通过具有概念模型(对象)、存储架构(数据库)和两者之间映射的内存中表示形式,导致面向对象编程和关系数据库之间的阻抗不匹配问题。 此元数据称为实体数据模型,简称 EDM。 从这个 EDM 中,实体框架将从内存中的对象派生到往返数据的视图到数据库并返回。

当实体框架与正式指定概念模型、存储架构和映射的 EDMX 文件一起使用时,模型加载阶段只需验证 EDM 是否正确(例如,确保没有缺少映射),然后生成视图,然后验证视图并准备好使用此元数据。 只有这样,才能执行查询或将新数据保存到数据存储。

代码优先方法的核心是一个复杂的实体数据模型生成器。 实体框架必须从提供的代码生成 EDM;它通过分析模型中涉及的类、应用约定并通过 Fluent API 配置模型来达到这一要求。 构建 EDM 后,实体框架的工作方式与项目中存在 EDMX 文件的方式相同。 因此,从 Code First 构建模型会增加额外的复杂性,与使用 EDMX 相比,实体框架的启动时间会变慢。 成本完全取决于正在构建的模型的大小和复杂性。

在选择使用 EDMX 与代码优先时,请务必了解代码 First 引入的灵活性会增加首次构建模型的成本。 如果应用程序能够承受此首次加载的成本,那么通常 Code First 将是首选方式。

10 调查性能10 Investigating Performance

10.1 使用可视化工作室探查器10.1 Using the Visual Studio Profiler

如果实体框架存在性能问题,则可以使用 Visual Studio 中内置的探查器来查看应用程序花费的时间。 这是我们用于在”探索ADO.NET实体框架的性能 - 第 1 部分”博客文章(https://docs.microsoft.com/archive/blogs/adonet/exploring-the-performance-of-the-ado-net-entity-framework-part-1)显示实体框架在冷热查询期间花费时间)中生成饼图的工具。

数据和建模客户咨询团队编写的”使用 Visual Studio 2010 探查器分析实体框架”博客文章显示了他们如何使用探查器调查性能问题的真实示例。https://docs.microsoft.com/archive/blogs/dmcat/profiling-entity-framework-using-the-visual-studio-2010-profiler. 这篇文章是为窗口应用程序编写的。 如果需要分析 Web 应用程序,Windows 性能记录仪 (WPR) 和 Windows 性能分析器 (WPA) 工具可能比在 Visual Studio 工作更好。 WPR 和 WPA 是 Windows 性能工具包的一部分,该工具包包含在 Windowshttp://www.microsoft.com/download/details.aspx?id=39982评估和部署工具包 () 中。

10.2 应用程序/数据库分析10.2 Application/Database profiling

像可视化工作室中内置的探查器这样的工具告诉您应用程序将花时间的位置。另一种类型的探查器可用于根据需求在生产或预生产中对正在运行的应用程序进行动态分析,并查找常见的陷阱和数据库访问的抗模式。

两个市售的探查器是实体框架探查http://efprof.com)器(和 ORMProfiler(。 < http://ormprofiler.com>)

如果您的应用程序是使用代码优先的 MVC 应用程序,则可以使用 StackExchange 的 Mini Profiler。 斯科特·汉塞尔曼在他的博客中描述了这个工具: < http://www.hanselman.com/blog/NuGetPackageOfTheWeek9ASPNETMiniProfilerFromStackExchangeRocksYourWorld.aspx>.

有关分析应用程序的数据库活动的详细信息,请参阅 Julie Lerman 的 MSDN 杂志文章,标题为实体框架中的分析数据库活动

10.3 数据库记录器10.3 Database logger

如果使用实体框架 6,还考虑使用内置日志记录功能。 可以指示上下文的数据库属性通过简单的单行配置记录其活动:

  1. using (var context = newQueryComparison.DbC.NorthwindEntities())
  2. {
  3. context.Database.Log = Console.WriteLine;
  4. var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
  5. q.ToList();
  6. }

在此示例中,数据库活动将记录到控制台,但可以将 Log 属性配置为调用任何操作<字符串>委托。

如果要在不重新编译的情况下启用数据库日志记录,并且正在使用实体框架 6.1 或更高版本,则可以通过在应用程序的 Web.config 或 app.config 文件中添加拦截器来执行此操作。

  1. <interceptors>
  2. <interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework">
  3. <parameters>
  4. <parameter value="C:\Path\To\My\LogOutput.txt"/>
  5. </parameters>
  6. </interceptor>
  7. </interceptors>

有关如何在不重新编译的情况下添加日志记录的详细信息,http://blog.oneunicorn.com/2014/02/09/ef-6-1-turning-on-logging-without-recompiling/请访问 。

11 附录11 Appendix

11.1 A. 测试环境11.1 A. Test Environment

此环境使用 2 台计算机设置,数据库与客户端应用程序分开的计算机上。 计算机位于同一机架中,因此网络延迟相对较低,但比单机环境更逼真。

11.1.1 应用服务器11.1.1 App Server

11.1.1.1 软件环境11.1.1.1 Software Environment
  • 实体框架 4 软件环境
    • 操作系统名称:Windows 服务器 2008 R2 企业 SP1。
    • 视觉工作室 2010 – 终极。
    • 可视化工作室 2010 SP1 (仅适用于某些比较)。
  • 实体框架 5 和 6 软件环境
    • 操作系统名称:Windows 8.1 企业版
    • 视觉工作室 2013 – 终极。
11.1.1.2 硬件环境11.1.1.2 Hardware Environment
  • 双处理器:英特尔(R) Xeon(R) CPU L5520 W3530 = 2.27GHz,2261 Mhz8 GHz,4 核(s),84 逻辑处理器。
  • 2412 GB RamRAM。
  • 136 GB SCSI250GB SATA 7200 rpm 3GB/s 驱动器拆分为 4 个分区。

11.1.2 数据库服务器11.1.2 DB server

11.1.2.1 软件环境11.1.2.1 Software Environment
  • 操作系统名称:Windows 服务器 2008 R28.1 企业 SP1。
  • SQL 服务器 2008 R22012。
11.1.2.2 硬件环境11.1.2.2 Hardware Environment
  • 单处理器:英特尔(R) Xeon(R) CPU L5520 = 2.27GHz,2261 MhzES-1620 0 = 3.60GHz,4 个核心(s),8 个逻辑处理器。
  • 824 GB 拉姆拉姆。
  • 465 GB ATA500GB SATA 7200 rpm 6GB/s 驱动器拆分为 4 个分区。

11.2 B. 查询性能比较测试11.2 B. Query performance comparison tests

北风模型用于执行这些测试。 它是使用实体框架设计器从数据库生成的。 然后,使用以下代码比较查询执行选项的性能:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Data.Common;
  5. using System.Data.Entity.Infrastructure;
  6. using System.Data.EntityClient;
  7. using System.Data.Objects;
  8. using System.Linq;
  9. namespace QueryComparison
  10. {
  11. public partial class NorthwindEntities : ObjectContext
  12. {
  13. private static readonly Func<NorthwindEntities, string, IQueryable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
  14. (NorthwindEntities context, string categoryName) =>
  15. context.Products.Where(p => p.Category.CategoryName == categoryName)
  16. );
  17. public IQueryable<Product> InvokeProductsForCategoryCQ(string categoryName)
  18. {
  19. return productsForCategoryCQ(this, categoryName);
  20. }
  21. }
  22. public class QueryTypePerfComparison
  23. {
  24. private static string entityConnectionStr = @"metadata=res://*/Northwind.csdl|res://*/Northwind.ssdl|res://*/Northwind.msl;provider=System.Data.SqlClient;provider connection string='data source=.;initial catalog=Northwind;integrated security=True;multipleactiveresultsets=True;App=EntityFramework'";
  25. public void LINQIncludingContextCreation()
  26. {
  27. using (NorthwindEntities context = new NorthwindEntities())
  28. {
  29. var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
  30. q.ToList();
  31. }
  32. }
  33. public void LINQNoTracking()
  34. {
  35. using (NorthwindEntities context = new NorthwindEntities())
  36. {
  37. context.Products.MergeOption = MergeOption.NoTracking;
  38. var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
  39. q.ToList();
  40. }
  41. }
  42. public void CompiledQuery()
  43. {
  44. using (NorthwindEntities context = new NorthwindEntities())
  45. {
  46. var q = context.InvokeProductsForCategoryCQ("Beverages");
  47. q.ToList();
  48. }
  49. }
  50. public void ObjectQuery()
  51. {
  52. using (NorthwindEntities context = new NorthwindEntities())
  53. {
  54. ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");
  55. products.ToList();
  56. }
  57. }
  58. public void EntityCommand()
  59. {
  60. using (EntityConnection eConn = new EntityConnection(entityConnectionStr))
  61. {
  62. eConn.Open();
  63. EntityCommand cmd = eConn.CreateCommand();
  64. cmd.CommandText = "Select p From NorthwindEntities.Products As p Where p.Category.CategoryName = 'Beverages'";
  65. using (EntityDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
  66. {
  67. List<Product> productsList = new List<Product>();
  68. while (reader.Read())
  69. {
  70. DbDataRecord record = (DbDataRecord)reader.GetValue(0);
  71. // 'materialize' the product by accessing each field and value. Because we are materializing products, we won't have any nested data readers or records.
  72. int fieldCount = record.FieldCount;
  73. // Treat all products as Product, even if they are the subtype DiscontinuedProduct.
  74. Product product = new Product();
  75. product.ProductID = record.GetInt32(0);
  76. product.ProductName = record.GetString(1);
  77. product.SupplierID = record.GetInt32(2);
  78. product.CategoryID = record.GetInt32(3);
  79. product.QuantityPerUnit = record.GetString(4);
  80. product.UnitPrice = record.GetDecimal(5);
  81. product.UnitsInStock = record.GetInt16(6);
  82. product.UnitsOnOrder = record.GetInt16(7);
  83. product.ReorderLevel = record.GetInt16(8);
  84. product.Discontinued = record.GetBoolean(9);
  85. productsList.Add(product);
  86. }
  87. }
  88. }
  89. }
  90. public void ExecuteStoreQuery()
  91. {
  92. using (NorthwindEntities context = new NorthwindEntities())
  93. {
  94. ObjectResult<Product> beverages = context.ExecuteStoreQuery<Product>(
  95. @" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
  96. FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
  97. WHERE (C.CategoryName = 'Beverages')"
  98. );
  99. beverages.ToList();
  100. }
  101. }
  102. public void ExecuteStoreQueryDbContext()
  103. {
  104. using (var context = new QueryComparison.DbC.NorthwindEntities())
  105. {
  106. var beverages = context.Database.SqlQuery\<QueryComparison.DbC.Product>(
  107. @" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
  108. FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
  109. WHERE (C.CategoryName = 'Beverages')"
  110. );
  111. beverages.ToList();
  112. }
  113. }
  114. public void ExecuteStoreQueryDbSet()
  115. {
  116. using (var context = new QueryComparison.DbC.NorthwindEntities())
  117. {
  118. var beverages = context.Products.SqlQuery(
  119. @" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
  120. FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
  121. WHERE (C.CategoryName = 'Beverages')"
  122. );
  123. beverages.ToList();
  124. }
  125. }
  126. public void LINQIncludingContextCreationDbContext()
  127. {
  128. using (var context = new QueryComparison.DbC.NorthwindEntities())
  129. {
  130. var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
  131. q.ToList();
  132. }
  133. }
  134. public void LINQNoTrackingDbContext()
  135. {
  136. using (var context = new QueryComparison.DbC.NorthwindEntities())
  137. {
  138. var q = context.Products.AsNoTracking().Where(p => p.Category.CategoryName == "Beverages");
  139. q.ToList();
  140. }
  141. }
  142. }
  143. }

11.3 C. 纳维视觉模型11.3 C. Navision Model

Navision 数据库是一个大型数据库,用于演示 Microsoft 动态和导航。 生成的概念模型包含 1005 个实体集和 4227 个关联集。 测试中使用的模型为”平面” - 未向其添加继承。

11.3.1 用于 Navision 测试的查询11.3.1 Queries used for Navision tests

与 Navision 模型一起使用的查询列表包含 3 类实体 SQL 查询:

11.3.1.1 查找11.3.1.1 Lookup

没有聚合的简单查找查询

  • 数量: 16232
  • 示例:
  1. <Query complexity="Lookup">
  2. <CommandText>Select value distinct top(4) e.Idle_Time From NavisionFKContext.Session as e</CommandText>
  3. </Query>
11.3.1.2 单聚合11.3.1.2 SingleAggregating

具有多个聚合但没有子值(单个查询)的普通 BI 查询

  • 数量: 2313
  • 示例:
  1. <Query complexity="SingleAggregating">
  2. <CommandText>NavisionFK.MDF_SessionLogin_Time_Max()</CommandText>
  3. </Query>

其中 MDF__会话_登录时间最大值() 在模型中定义为:

  1. <Function Name="MDF_SessionLogin_Time_Max" ReturnType="Collection(DateTime)">
  2. <DefiningExpression>SELECT VALUE Edm.Min(E.Login_Time) FROM NavisionFKContext.Session as E</DefiningExpression>
  3. </Function>
11.3.1.3 聚合子总计11.3.1.3 AggregatingSubtotals

包含聚合和小计的 BI 查询(通过全部联合)

  • 计数: 178
  • 示例:
  1. <Query complexity="AggregatingSubtotals">
  2. <CommandText>
  3. using NavisionFK;
  4. function AmountConsumed(entities Collection([CRONUS_International_Ltd__Zone])) as
  5. (
  6. Edm.Sum(select value N.Block_Movement FROM entities as E, E.CRONUS_International_Ltd__Bin as N)
  7. )
  8. function AmountConsumed(P1 Edm.Int32) as
  9. (
  10. AmountConsumed(select value e from NavisionFKContext.CRONUS_International_Ltd__Zone as e where e.Zone_Ranking = P1)
  11. )
  12. ----------------------------------------------------------------------------------------------------------------------
  13. (
  14. select top(10) Zone_Ranking, Cross_Dock_Bin_Zone, AmountConsumed(GroupPartition(E))
  15. from NavisionFKContext.CRONUS_International_Ltd__Zone as E
  16. where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed
  17. group by E.Zone_Ranking, E.Cross_Dock_Bin_Zone
  18. )
  19. union all
  20. (
  21. select top(10) Zone_Ranking, Cast(null as Edm.Byte) as P2, AmountConsumed(GroupPartition(E))
  22. from NavisionFKContext.CRONUS_International_Ltd__Zone as E
  23. where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed
  24. group by E.Zone_Ranking
  25. )
  26. union all
  27. {
  28. Row(Cast(null as Edm.Int32) as P1, Cast(null as Edm.Byte) as P2, AmountConsumed(select value E
  29. from NavisionFKContext.CRONUS_International_Ltd__Zone as E
  30. where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed))
  31. }</CommandText>
  32. <Parameters>
  33. <Parameter Name="MinAmountConsumed" DbType="Int32" Value="10000" />
  34. </Parameters>
  35. </Query>