性能与 Entity Framework






4.86/5 (55投票s)
关于Entity Framework性能最佳实践的文章
引言
如果你正在使用Entity Framework (EF),那么你需要了解提高其性能的最佳实践,否则你将承担后果!
背景
我的团队在企业应用程序中(包括测试版)使用了Entity Framework的第一个版本近两年。我们的应用程序采用面向服务的架构 (SOA),使用 .NET 3.5 Framework SP1、SQL Server 2008 和 Windows Server 2008 上的 IIS 7.0。在开发过程中,我们遇到了严重的性能问题,通过寻找和遵循一些最佳实践,我们克服了这些问题。本文旨在解释这些最佳实践。本文假设您熟悉 EF 是什么以及如何使用它。如果您需要更多介绍性概述,请查看Entity Framework 简介文章。
最佳实践
我们的应用程序有一个包含 400 多个表的数据库,其中一些表之间高度互连。我们所有的 Entity Framework 调用都包含在我们的 Web 服务项目中。我们的许多 EF 调用在首次调用时需要 8.5 秒才能完成。通过对我们的 Web 服务项目进行某些更改(见下表),我们能够将这些调用改进到 3 秒。第二次 EF 调用几乎是即时的。下表显示了 EF 的整体性能影响以及我们为实现这些更改所需的重构级别。
最佳实践 | 影响 | 所需重构 |
预生成视图 | 主要 | 次要 |
使用小型 EDMX 文件 | 次要 | 中 |
创建智能连接字符串 | 次要 | 次要 |
禁用更改跟踪 | 次要 | 次要 |
使用编译查询 | 中 | 中 |
小心使用 Includes | 中 | 主要 |
本文接下来的部分将更详细地解释这些最佳实践——请继续阅读!
预生成视图
最昂贵的操作(它占初始查询的 50% 以上)是“视图生成”。创建数据库抽象视图的一个重要部分是提供用于在存储的本机语言中进行查询和更新的实际视图。在视图生成期间,会创建存储视图。幸运的是,我们可以通过使用视图生成命令参数 /mode:ViewGeneration
运行 EDM 生成器 (EdmGen.exe) 命令行工具来消除构建内存中视图的开销(参见下文)。
运行此工具将创建一个包含预生成视图的代码文件(可以是 .cs 或 .vb),可以将其包含并编译到您的项目中。预生成视图可以显著缩短启动时间。首次创建 EF ObjectContext
时必须生成视图,因此预生成视图并将其与您的应用程序一起部署非常重要。这样做的不利之处是,您必须使生成的视图与您对数据模型所做的任何更改保持同步。
"%windir%\Microsoft.NET\Framework\v3.5\EdmGen.exe"
/mode:ViewGeneration
/language:CSharp
/nologo
"/inssdl:MyEntityModel.ssdl"
"/incsdl:MyEntityModel.csdl"
"/inmsl:MyEntityModel.msl"
"/outviews:MyEntityModel.Views.cs"
EdmGen.exe 工具安装在 .NET Framework 目录中。在许多情况下,它位于 C:\windows\Microsoft.NET\Framework\v3.5。对于 64 位系统,它位于 C:\windows\Microsoft.NET\Framework64\v3.5。您还可以从 Visual Studio 命令提示符访问 EdmGen.exe 工具(单击“开始”,指向“所有程序”,指向“Microsoft Visual Studio 2008”,指向“Visual Studio 工具”,然后单击“Visual Studio 2008 命令提示符”)。为了获取输入 .ssdl、.csdl 和 .msl 文件,您需要使用 Visual Studio 执行以下操作:
- 打开包含 Entity Framework 项目的项目。
- 在设计器中打开 EDMX 文件。
- 单击模型的背景以打开模型的“属性”窗口。
- 将“元数据工件处理”属性更改为:“复制到输出目录”。
- 保存项目——这将创建 .ssdl、.csdl 和 .msl 文件。
另一种方法是使用完整生成命令参数 /mode:FullGeneration
(参见下文)运行 EDM 生成器命令行工具。EdmGen.exe 仅需要一个有效的数据库连接字符串即可生成概念模型 (.csdl)、存储模型 (.ssdl) 和映射 (.msl) 文件以及预生成的视图文件。所有显示的选项都是必需的。
"%windir%\Microsoft.NET\Framework\v3.5\EdmGen.exe"
/mode:FullGeneration
/c:"Data Source=localhost;Initial Catalog=MyDatabase; Integrated Security=true"
/nologo
/language:CSharp
/entitycontainer:MyEntities
/namespace:MyModel
"/outviews:MyEntityModel.Views.cs"
"/outssdl:MyEntityModel.ssdl"
"/outcsdl:MyEntityModel.csdl"
"/outmsl:MyEntityModel.msl"
"/outobjectlayer:MyEntities.ObjectLayer.cs"
使用更小的 EDMX 文件
Entity Framework 能够处理大型实体数据模型,但如果数据模型高度互连,您可能会遇到性能问题。通常,当数据模型达到大约 100 个实体时,您就应该考虑将其分解(成多个 .edmx 文件)。
.xml 模式文件的大小与您从中生成模型的数据库中的表数量成正比。随着模式文件大小的增加,解析并为这些元数据创建内存中模型所需的时间也会增加。如前所述,这是每个 ObjectContext
实例一次性发生的开销,可以通过预生成视图来缩短。当实体数量过多时,Visual Studio Designer 的性能也会开始下降。拥有几个较小数据模型的缺点是,当您对数据库进行更改时,必须使各个 .edmx 文件保持同步。
智能连接字符串
ObjectContext
对象在创建对象查询时会自动从应用程序 config 文件中检索连接信息。您可以在实例化 ObjectContext
类时提供此命名连接字符串,而不是依赖 .config 文件中的 connectionString
参数。connectionString
参数中的 Metadata
属性包含 EntityClient
提供程序搜索 EDM 映射和元数据文件(.ssdl、.csdl 和 .msl 文件)的位置列表。
映射和元数据文件通常与应用程序可执行文件部署在同一目录中。这些映射文件也可以作为嵌入资源包含在应用程序中,这将提高性能。为了将 EDM 映射文件嵌入到程序集中,您需要使用 Visual Studio 执行以下操作:
- 打开包含 Entity Framework 项目的项目。
- 在设计器中打开 EDMX 文件。
- 单击模型的背景以打开模型的“属性”窗口。
- 将“元数据工件处理”属性更改为:“嵌入到输出程序集中”。
- 重新构建项目——这将构建一个包含 .ssdl、.csdl 和 .msl 信息的 .dll 文件。
嵌入资源指定如下:Metadata=res://<assemblyFullName>/<resourceName>
。请注意,当您对 assemblyFullName
使用通配符 (*) 时,Entity Framework 必须在调用、引用和并行程序集中查找具有正确名称的资源。为了提高性能,请始终指定程序集名称而不是通配符。下面是 web.config 文件中包含 connectionString
参数的摘录。您可以看到 metadata
属性指定了程序集名称 MyApp.MyService.BusinessEntities.dll。
<connectionStrings>
<add name="MyEntities"
connectionString="metadata=res://MyApp.ServiceRequest.BusinessEntities,
Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null/;provider=System.Data.SqlClient;
provider connection string="Data Source=localhost;Initial Catalog=MyDatabase;
Integrated Security=True;MultipleActiveResultSets=True""
providerName="System.Data.EntityClient" />
</connectionStrings>
禁用更改跟踪
一旦消除了视图生成开销,最昂贵的操作就是“对象物化”。此操作占用您 75% 的查询时间,因为它必须从 DbDataReader
对象读取并创建对象。当您使用 Entity Framework 时,您拥有的对象代表数据库中的表。这些对象由一个称为对象物化的内部过程创建。此过程获取返回的数据并为您构建相关对象。对象可以是 EntityObject
派生对象、匿名类型或 DbDataRecord DbDataRecord
。
ObjectContext
对象将创建一个 ObjectStateEntry
对象来帮助跟踪对相关实体所做的更改。当查询、添加或附加到此类的缓存引用时,对象将被跟踪。跟踪行为使用 MergeOption
枚举指定。当跟踪对象的属性发生更新时,这些属性将被标记为已修改,并且原始值将保留以执行回数据库的更新。这使用户能够根据对象本身编写代码并调用 SaveChanges
。
我们可以通过使用 MergeOption.NoTracking
选项来最小化更改跟踪的开销。在大多数情况下,这样做会提高系统的性能。如果您通过 Web 服务在网络上传输数据,则更改跟踪的丢失无关紧要,因为此功能在“断开连接”模式下不起作用。即使您没有断开连接,您也可以在不需要更新数据库的页面中使用此选项。请看下面的代码片段,了解如何禁用更改跟踪的示例:
public List<ServiceRequest> GetBySubmissionDate(DateTime startDate, DateTime endDate)
{
using (new Tracer("Trace"))
{
using (MyEntities db = new MyEntities())
{
ObjectQuery<ServiceRequest> query =
(ObjectQuery<ServiceRequest>)CompiledQueries.GetBySubmissionDate
(db, startDate, endDate);
// The .Execute call is using the NoTracking option
List<ServiceRequest> serviceRequests =
query.Execute(MergeOption.NoTracking).ToList();
return serviceRequests;
}
}
}
由于每个查询都不同,所以 MergeOption.NoTracking
选项对性能的帮助程度尚不清楚。总的来说,我认为这绝对值得一试。但请注意,使用 MergeOption.NoTracking
选项时存在以下注意事项:
- 没有身份解析,因此,根据您的查询,可能会出现主键相同但引用上不同的实体。
- 外键仅在两个实体都加载时才加载。如果关联的另一端未包含,则外键也不会加载。
- 跟踪对象会被缓存,因此后续对该对象的调用将不会访问数据库。如果您使用
NoTracking
并尝试多次加载同一个对象,则每次都会查询数据库。
使用编译查询
“查询创建”也占用了大量时间,在 EF 预热后大约占查询时间的 10%。查询的某些部分会被缓存,以便后续查询比第一次更快。但是并非所有查询部分都被缓存,导致某些部分在每次执行查询时都需要重新构建(除非您使用 EntitySql
)。我们可以通过使用编译查询来消除重新构建查询计划的需要。要为以后编译查询,您可以使用使用委托的 CompiledQuery.Compile
方法。
public static readonly Func<MyEntities, DateTime,
DateTime, IQueryable<ServiceRequest>> GetBySubmissionDate =
CompiledQuery.Compile<MyEntities, DateTime, DateTime, IQueryable<ServiceRequest>>(
(db, startDate, endDate) => (from s in db.ServiceRequest
.Include("ServiceRequestType")
.Include("ServiceRequestStatus")
where s.SubmissionDate >= startDate
&& s.SubmissionDate <= endDate
orderby s.SubmissionDate ascending
select s);
请注意,使用 CompiledQuery.Compile
选项时存在以下注意事项:
- 您不能将
CompiledQueries
与带参数的 Includes 一起使用 - 因此您必须改用EntitySQL
。 - 编译一个查询以供重用所需的时间至少是直接执行它而不进行缓存的两倍。
小心使用 Includes
您可以使用 .Include
语句指示框架进行关系的即时加载。通过即时加载,框架将构建一个连接查询,并且关系数据将与原始实体数据一起获取。.Include
语句可能会导致返回大量数据,也可能由于需要在数据存储中使用许多 Join
语句而导致复杂的查询。
public List<ServiceRequest> GetBySubmissionDate(DateTime startDate, DateTime endDate)
{
using (new Tracer("Trace"))
{
using (MyEntities db = new MyEntities())
{
// The compiled query uses .Include to eager
// load ServiceRequestType and ServiceRequestStatus
ObjectQuery<ServiceRequest> query = (ObjectQuery<ServiceRequest>)
CompiledQueries.GetBySubmissionDate(db, startDate, endDate);
List<ServiceRequest> serviceRequests = query.Execute
(MergeOption.NoTracking).ToList();
return serviceRequests;
}
}
}
这可以通过使用 .Load
语句来延迟加载关系来避免。通过延迟加载,每次调用加载以获取关系信息时都会构建并运行一个额外的查询。如果您只想延迟加载某些关系,您可以选择使用如下所示的代码:
public List<ServiceRequest> GetBySubmissionDate(DateTime startDate, DateTime endDate)
{
using (new Tracer("Trace"))
{
using (MyEntities db = new MyEntities())
{
ObjectQuery<ServiceRequest> query =
(ObjectQuery<ServiceRequest>)CompiledQueries.GetBySubmissionDate
(db, startDate, endDate);
List<ServiceRequest> serviceRequests =
query.Execute(MergeOption.NoTracking).ToList();
// Use the .Load statement to lazy load the ServiceRequestType
foreach (ServiceRequest item in serviceRequests)
{
if (!item.ServiceRequestTypeReference.IsLoaded)
item.ServiceRequestTypeReference.Load(MergeOption.NoTracking);
}
return serviceRequests;
}
}
}
请注意,在某些情况下,即时加载更好,而在另一些情况下,延迟加载是更好的方法。在下面的代码中,每个与指定 entity
参数具有关系的表都将进行延迟加载。
public static void LoadRelated(EntityObject entity, MergeOption mergeOption)
{
System.Reflection.PropertyInfo[] properties = entity.GetType().GetProperties();
foreach (System.Reflection.PropertyInfo propInfo in properties)
{
if (IsEntityReference(propInfo))
{
EntityReference er = (EntityReference)propInfo.GetValue(entity, null);
if (!er.IsLoaded)
er.Load(mergeOption);
}
if (IsEntityCollection(propInfo))
{
//The actual stored value of the EntityCollection is a RelatedEnd object.
System.Data.Objects.DataClasses.RelatedEnd end =
(System.Data.Objects.DataClasses.RelatedEnd)propInfo.GetValue(entity, null);
if (!end.IsLoaded)
{
end.Load(mergeOption);
//Get the enumerator off the RelatedEnd object so we can cycle
//through items in EntityCollection without knowing the type
System.Collections.IEnumerator enumerator = end.GetEnumerator();
//Cycle through items in EntityCollection and add them to the list.
while (enumerator.MoveNext())
{
LoadChildren(enumerator.Current as EntityObject);
}
}
}
}
}
您可能希望查看关于Entity Framework 透明延迟加载的系列文章,以获取使用 EFLazyLoading 库的更具可读性和更简单的延迟加载版本。
关注点
在使用 Entity Framework 时,性能方面需要牢记以下几点:
ObjectContext
的初始创建包括加载和验证元数据的成本。- 任何查询的初始执行都包括构建查询缓存以实现后续查询更快执行的成本。
- 编译的 LINQ 查询比未编译的 LINQ 查询更快。
- 当不需要跟踪更改和关系时(例如通过网络发送数据),使用
NoTracking
合并选项执行的查询效果很好。 - 您可能希望查看 EdmGen2.exe 工具,它可以替代原始的 EdmGen.exe 工具,并且能够读写 EDMX 文件。
- 如果您尝试导航未加载的多对一或一对一关系,您将得到
NullReferenceException
。
致谢
特别感谢 Alex Creech 在 Visual Studio Profiler 中处理数据并编写我们大部分 Entity Framework 助手代码!
修订历史
- 2009年8月11日 - 初版