65.9K
CodeProject 正在变化。 阅读更多。
Home

性能与 Entity Framework

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (30投票s)

2011年10月16日

CPOL

9分钟阅读

viewsIcon

52983

一篇介绍 Entity Framework 性能最佳实践的文章。

引言

如果您正在使用 Entity Framework (EF),那么您需要了解提高其性能的最佳实践,否则您将承受后果!

背景

我的团队在企业应用程序中(包括 beta 版)花了将近两年时间使用第一版 Entity Framework。我们的应用程序采用面向服务架构 (SOA),使用 .NET 3.5 Framework SP1、SQL Server 2008 以及 Windows Server 2008 上的 IIS 7.0。在开发过程中,我们遇到了严重的性能问题,通过寻找并遵循一些最佳实践,我们解决了这些问题。本文旨在解释这些实践。本文假设您已经熟悉 EF 是什么以及如何使用它。

Using the Code

我们的应用程序有一个包含 400 多个表的数据库,其中一些表之间存在高度关联。我们所有的 Entity Framework 调用都包含在我们的 Web Service 项目中。许多 EF 调用在首次调用时需要 8.5 秒才能完成。通过对我们的 Web Service 项目进行某些更改(见下表),我们将这些调用的时间缩短到 3 秒。第二次调用 EF 几乎是即时的。下表显示了 EF 的整体性能影响以及实施更改所需的重构级别。

更改类型 影响 所需重构
预生成视图 主要 次要
使用较小的 EDMX 文件 次要
禁用更改跟踪 次要 次要
使用编译的查询
注意 Include 的使用 主要
智能连接字符串 次要 次要

预生成视图

最昂贵的操作(占初始查询的 50% 以上)是视图生成。抽象数据库视图的一部分是为查询和更新提供存储的本地语言的实际视图。在视图生成期间,会创建存储视图。幸运的是,我们可以通过运行 EDM 生成器 (EdmGen.exe) 命令行工具并使用视图生成命令参数/mode:ViewGeneration(见下文)来消除构建内存视图的开销。

运行此工具将创建一个代码文件(.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 文件,您需要执行以下操作:

  1. 打开包含 Entity Framework 项目的项目。
  2. 在设计器中打开 EDMX 文件。
  3. 单击模型的背景以打开模型的属性窗口。
  4. 将“元数据构件处理”属性更改为:“复制到输出目录”。
  5. 保存项目 - 这将创建.ssdl.csdl.msl 文件。

另一种方法是运行 EDM 生成器命令行工具并使用完整生成命令参数/mode:FullGeneration(见下文)。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 实例一次性发生的成本,可以通过预生成视图来缩短。当实体数量过多时,设计器也会开始出现性能问题。拥有多个较小数据模型的缺点是,在对数据库进行更改时,您必须使各个.edmx 文件保持同步。

禁用更改跟踪

一旦消除了视图生成成本,最昂贵的操作就是对象物化。此操作会消耗 75% 的查询时间,因为它必须从 DbDataReader 对象读取并创建对象。当您使用 Entity Framework 时,您拥有代表数据库表的各种对象。这些对象是由一个称为对象物化的内部过程创建的。此过程会获取返回的数据并为您构建相关的对象。对象可以是 EntityObject 派生对象、匿名类型或 DbDataRecord

ObjectContext 对象将创建一个 ObjectStateEntry 对象来帮助跟踪对相关实体所做的更改。当查询、添加或附加到此类的缓存引用中的对象时,将对其进行跟踪。使用 MergeOption 枚举指定跟踪行为。当跟踪的对象属性发生更新时,这些属性将被标记为已修改,并且会保留原始值以用于将更新写回数据库。这使用户能够直接对对象编写代码并调用 SaveChanges

通过使用 MergeOption.NoTracking 选项,我们可以最大限度地减少更改跟踪的开销。在大多数情况下,这样做将提高系统的性能。丢失的更改跟踪在通过 Web Service 将数据发送到网络时无关紧要,因为此功能在“断开连接”模式下不起作用。即使您没有断开连接,也可以在不更新数据库的页面中使用此选项。请看下面的代码片段,其中包含如何禁用更改跟踪的示例。

public MyObject GetById(Guid requestId, MyApp.Common.EntityModel.EntityArgs args)
{
    if (null == args)
        throw new ArgumentNullException("args");

    using (new Tracer("Trace"))
    {
        using (MyEntities db = new MyEntities())
        {
            ObjectQuery<myobject> query = 
              (ObjectQuery<myobject>)CompiledQueries.GetById(db, requestId);
            
            MyObject sr = query.Execute(MergeOption.NoTracking).FirstOrDefault();

            if (args.LoadRelated)
            {
                MyApp.Common.EntityModel.EntityUtils.LoadChildren(sr);
            }
            
            return sr;
        }
    }
}

由于每次查询都不同,因此 MergeOption.NoTracking 选项对性能的提升程度尚不清楚。总的来说,我认为绝对值得一试。但请注意,使用 MergeOption.NoTracking 选项存在以下注意事项:

  • 没有身份解析,因此,根据您的查询,您可能会获得主键相同但引用不同的实体。
  • 仅当同时加载两个实体时,才会加载外键。如果关联的另一端未包含在内,则外键也不会加载。
  • 已跟踪的对象会被缓存,因此后续对该对象的调用将不会命中数据库。如果您使用 NoTracking 并尝试多次加载同一对象,每次都会查询数据库。

使用编译的查询

查询创建也占用了大量时间,在 EF 预热后,约占查询时间的 10%。查询的某些部分会被缓存,以便后续查询比第一次执行更快。但是,并非所有查询部分都会被缓存,一些部分需要每次执行查询时重新构建(除非您使用 eSQL)。我们可以使用编译的查询来消除重新构建查询计划的需要。要为以后编译查询,您可以使用 CompiledQuery.Compile 方法,该方法使用委托。

public static readonly Func<myentities,>> GetById =
    CompiledQuery.Compile<myentities,>>(
    (db, requestId) => (from s in db.ServiceRequest
     .Include("ServiceRequestType")
     .Include("ServiceRequestStatus")
     where s.ServiceRequestId == requestId
    select s));

注意:使用 CompiledQuery.Compile 选项时存在以下注意事项:

  • 您不能将 CompiledQuery 与参数化 Include 一起使用,因此您必须改用 EntitySQL。
  • 编译一个用于重用的查询至少需要执行该查询而不进行缓存的时间的两倍。

注意 Include 的使用

您可以通过使用 .Include 语句(如上一节所示)指示框架急切加载关系。通过急切加载,框架将构建一个 JOIN 查询,并将关系数据与原始实体数据一起获取。这可以通过使用 .Load 语句来延迟加载关系来避免。通过延迟加载,将构建一个额外的查询并为每次加载调用运行,以获取关系信息。请注意,在某些情况下,急切加载更好,而在其他情况下,延迟加载是更好的方法。.Include 语句可能导致检索大量数据,并且由于需要在数据存储中使用许多 Join 语句,因此可能导致复杂查询。在下面的代码中,将加载与指定表有关系的所有表。

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);
                }
            }
        }
    }
}

智能连接字符串

ObjectContext 对象在创建对象查询时会自动从应用程序配置文件检索连接信息。在实例化 ObjectContext 类时,您可以提供此命名连接字符串,而不是依赖于.config 文件中的 connectionString 参数。connectionString 参数中的 Metadata 属性包含一个 EntityClient 提供程序搜索 EDM 映射和元数据文件(.ssdl.csdl.msl 文件)的列表。映射和元数据文件通常部署在与应用程序可执行文件相同的目录中。这些映射文件也可以包含在应用程序中作为嵌入资源,这将提高性能。为了将 EDM 映射文件嵌入程序集,您需要执行以下操作:

  1. 打开包含 Entity Framework 项目的项目。
  2. 在设计器中打开 EDMX 文件。
  3. 单击模型的背景以打开模型的属性窗口。
  4. 将“元数据构件处理”属性更改为:“嵌入到输出程序集中”。
  5. 重新生成项目 - 这将构建一个包含.ssdl.csdl.msl 信息的.dll 文件。

嵌入式资源指定如下:Metadata=res://<assemblyFullName>/<resourceName>。注意:当您对 assemblyFullName 使用通配符 (*) 时,Entity Framework 必须搜索调用、引用和旁加载的程序集以查找具有正确名称的资源。为了提高性能,请始终指定程序集名称而不是通配符。下面是一个包含 connectionString 参数的web.config 文件的摘录。您可以看到 metadata 属性指定了程序集名称MyApp.MyService.BusinessEntities.dll

<connectionStrings>
    <add name="MyEntities" 
      connectionString="metadata=res://MyApp.MyService.BusinessEntities, 
        Version=1.0.0.0, Culture=neutral, 
        PublicKeyToken=null/;provider=System.Data.SqlClient;provider 
        connection string="Data Source=localhost;Initial Catalog=MyDB;
     Integrated Security=True;MultipleActiveResultSets=True"" 
     providerName="System.Data.EntityClient" />
</connectionStrings>

关注点

在使用 Entity Framework 时,有几点需要记住:

  • 首次创建 ObjectContext 包括加载和验证元数据的成本。
  • 任何查询的首次执行包括构建查询缓存的成本,以加快后续查询的执行速度。
  • 编译的 LINQ 查询比非编译的 LINQ 查询速度更快。
  • 当不需要跟踪更改和关系时(例如,通过网络发送的数据),使用 NoTracking 合并选项执行的查询效果很好。

致谢

特别感谢 Alex Creech 在 Visual Studio Profiler 中进行数字分析并编写了我们许多 Entity Framework 辅助代码!

修订历史

  • 2009 年 8 月 11 日 - 初稿。
© . All rights reserved.