在 ASP.NET 4 Web 应用程序中使用 Entity Framework 4.0 最大程度地提高性能

作者 :Tom Dykstra

本教程系列基于由入门使用 Entity Framework 4.0 教程系列创建的 Contoso University Web 应用程序。 如果未完成前面的教程,作为本教程的起点,可以下载已创建 的应用程序 。 还可以下载完整教程系列创建 的应用程序 。 如果对教程有疑问,可将其发布到 ASP.NET 实体框架论坛

在上一教程中,你已了解如何处理并发冲突。 本教程介绍用于提高使用实体框架的 ASP.NET Web 应用程序的性能的选项。 你将了解几种方法,用于最大程度地提高性能或诊断性能问题。

以下部分中提供的信息在各种方案中可能很有用:

  • 高效加载相关数据。
  • 管理视图状态。

如果存在出现性能问题的单个查询,以下部分中提供的信息可能很有用:

  • 使用 NoTracking 合并选项。
  • 预编译 LINQ 查询。
  • 检查发送到数据库的查询命令。

以下部分中提供的信息对于具有极大型数据模型的应用程序可能有用:

  • 预生成视图。

注意

Web 应用程序性能受到许多因素的影响,包括请求和响应数据的大小、数据库查询的速度、服务器可排队的请求数以及服务速度,甚至可能正在使用的任何客户端脚本库的效率。 如果应用程序的性能至关重要,或者测试或体验显示应用程序性能不尽如人意,则应遵循常规协议进行性能优化。 测量以确定性能瓶颈的发生位置,然后解决对应用程序整体性能影响最大的方面。

本主题主要侧重于在 ASP.NET 中可能提高实体框架性能的方法。 如果确定数据访问是应用程序中的性能瓶颈之一,则此处的建议非常有用。 除非另有说明,否则此处介绍的方法一般不应被视为“最佳做法”,其中许多方法仅适用于特殊情况或解决非常特定种类的性能瓶颈。

若要开始本教程,请启动 Visual Studio 并打开在上一教程中正在使用的 Contoso University Web 应用程序。

实体框架可通过多种方式将相关数据加载到实体的导航属性中:

  • 延迟加载。 首次读取实体时,不检索相关数据。 然而,首次尝试访问导航属性时,会自动检索导航属性所需的数据。 这会导致向数据库发送多个查询 - 一个查询用于实体本身,一次用于必须检索实体的相关数据。

    Image05

预先加载。 读取该实体时,会同时检索相关数据。 此时通常会出现单一联接查询,检索所有必需数据。 可以使用 Include 方法指定预先加载,如这些教程中所述。

Image07

  • 显式加载。 这类似于延迟加载,只不过是在代码中显式检索相关数据;访问导航属性时,它不会自动发生。 使用集合导航属性的 方法手动 Load 加载相关数据,或者对保存单个对象的属性使用 Load 引用属性的 方法。 (例如,调用 PersonReference.Load 方法来加载 PersonDepartment entity.)

    Image06

由于它们不会立即检索属性值,因此延迟加载和显式加载也称为 延迟加载

延迟加载是设计器生成的对象上下文的默认行为。 如果打开 SchoolModel.Designer。定义对象上下文类的 cs 文件,你将找到三个构造函数方法,每个方法都包含以下语句:

this.ContextOptions.LazyLoadingEnabled = true;

通常,如果知道需要检索到的每个实体的相关数据,则预先加载可提供最佳性能,因为发送到数据库的单个查询通常比针对检索的每个实体的单独查询更有效。 另一方面,如果只需要很少访问实体的导航属性,或者只需要访问一小部分实体的导航属性,则延迟加载或显式加载可能更有效,因为预先加载会检索比所需更多的数据。

在 Web 应用程序中,延迟加载可能价值相对较小,因为影响相关数据需求的用户操作发生在浏览器中,浏览器与呈现页面的对象上下文没有连接。 另一方面,在对控件进行数据绑定时,通常知道需要哪些数据,因此通常最好根据每个方案中的合适内容选择预先加载或延迟加载。

此外,数据绑定控件可能在释放对象上下文后使用实体对象。 在这种情况下,尝试延迟加载导航属性会失败。 收到的错误消息是明确的:“The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.

默认情况下, EntityDataSource 控件禁用延迟加载。 ObjectDataSource对于当前教程 (所使用的控件,或者如果从页面代码) 访问对象上下文,有几种方法可以默认禁用延迟加载。 可以在实例化对象上下文时禁用它。 例如,可以将以下行添加到 类的 SchoolRepository 构造函数方法:

context.ContextOptions.LazyLoadingEnabled = false;

对于 Contoso University 应用程序,你将使对象上下文自动禁用延迟加载,以便在实例化上下文时不必设置此属性。

打开 SchoolModel.edmx 数据模型,单击设计图面,然后在属性窗格中将 “延迟加载启用 ”属性设置为 False。 保存并关闭数据模型。

Image04

管理视图状态

为了提供更新功能,ASP.NET 网页必须在呈现页面时存储实体的原始属性值。 在回发处理期间,控件可以重新创建实体的原始状态,并在应用更改和调用 方法之前调用实体 AttachSaveChanges 方法。 默认情况下,ASP.NET Web Forms数据控件使用视图状态来存储原始值。 但是,视图状态可能会影响性能,因为它存储在隐藏字段中,这些字段会大大增加发送到浏览器和从浏览器发送的页面的大小。

用于管理视图状态或替代方法(如会话状态)的技术不是实体框架独有的,因此本教程不会详细介绍本主题。 有关详细信息,请参阅本教程末尾的链接。

但是,ASP.NET 版本 4 提供了一种处理视图状态的新方法,每个Web Forms应用程序的 ASP.NET 开发人员都应该注意:ViewStateMode属性。 可以在页面或控件级别设置此新属性,它使你能够默认禁用页面的视图状态,并仅对需要它的控件启用它。

对于性能至关重要的应用程序,一个好的做法是始终在页面级别禁用视图状态,并仅为需要它的控件启用它。 此方法不会大幅减少 Contoso University 页面中视图状态的大小,但要了解其工作原理,你将针对 Instructors.aspx 页面执行此操作。 该页包含许多控件,包括 Label 禁用视图状态的控件。 此页上的控件实际上都不需要启用视图状态。 (DataKeyNames 控件的 GridView 属性指定必须在回发之间保持的状态,但这些值将保持控件状态,不受 属性的影响 ViewStateMode 。)

指令 PageLabel 控件标记当前类似于以下示例:

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
    CodeBehind="Instructors.aspx.cs" Inherits="ContosoUniversity.Instructors" %>
    ...
    <asp:Label ID="ErrorMessageLabel" runat="server" Text="" Visible="false" ViewStateMode="Disabled"></asp:Label> 
    ...

进行以下更改:

  • 将 添加到 ViewStateMode="Disabled"Page 指令。
  • 从 控件中删除 ViewStateMode="Disabled"Label

标记现在类似于以下示例:

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
    CodeBehind="Instructors.aspx.cs" Inherits="ContosoUniversity.Instructors" 
    ViewStateMode="Disabled" %>
    ...
    <asp:Label ID="ErrorMessageLabel" runat="server" Text="" Visible="false"></asp:Label> 
    ...

现在,所有控件的视图状态都处于禁用状态。 如果以后添加的控件确实需要使用视图状态,则只需包含 ViewStateMode="Enabled" 该控件的 属性。

使用 NoTracking 合并选项

当对象上下文检索数据库行并创建表示它们的实体对象时,默认情况下,它还使用其对象状态管理器跟踪这些实体对象。 此跟踪数据充当缓存,并在更新实体时使用。 由于 Web 应用程序通常具有生存期较短的对象上下文实例,因此查询通常返回不需要跟踪的数据,因为读取它们的对象上下文将在再次使用或更新它读取的任何实体之前被释放。

在实体框架中,可以通过设置 合并选项来指定对象上下文是否跟踪实体对象。 可以为单个查询或实体集设置合并选项。 如果为实体集设置它,则意味着你正在为该实体集创建的所有查询设置默认合并选项。

对于 Contoso University 应用程序,从存储库访问的任何实体集都不需要跟踪,因此,在实例化存储库类中的对象上下文时,可以将这些实体集的合并选项设置为 NoTracking 。 (请注意,在本教程中,设置合并选项不会对应用程序的性能产生明显影响。选项 NoTracking 可能仅在某些高数据量方案中实现可观察的性能改进。)

在 DAL 文件夹中,打开 SchoolRepository.cs 文件并添加一个构造函数方法,该方法为存储库访问的实体集设置合并选项:

public SchoolRepository()
{
    context.Departments.MergeOption = MergeOption.NoTracking;
    context.InstructorNames.MergeOption = MergeOption.NoTracking;
    context.OfficeAssignments.MergeOption = MergeOption.NoTracking;
}

预编译 LINQ 查询

实体框架在给定 ObjectContext 实例的生命周期内首次执行实体 SQL 查询时,编译查询需要一些时间。 编译结果已缓存,这意味着查询的后续执行速度要快得多。 LINQ 查询遵循类似的模式,只不过编译查询所需的某些工作是在每次执行查询时完成的。 换句话说,对于 LINQ 查询,默认情况下不会缓存编译的所有结果。

如果 LINQ 查询希望在对象上下文的生命周期内重复运行,则可以编写代码,使第一次运行 LINQ 查询时缓存所有编译结果。

例如,你将对 类中的SchoolRepositoryGet个方法执行此操作,其中一个方法不采用任何参数 (方法) GetInstructorNames,一个方法需要参数 (GetDepartmentsByAdministrator方法) 。 这些方法现在实际上不需要编译,因为它们不是 LINQ 查询:

public IEnumerable<InstructorName> GetInstructorNames()
{
    return context.InstructorNames.OrderBy("it.FullName").ToList();
}
public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    return new ObjectQuery<Department>("SELECT VALUE d FROM Departments as d", context, MergeOption.NoTracking).Include("Person").Where(d => d.Administrator == administrator).ToList();
}

但是,若要尝试编译的查询,可以像将这些查询编写为以下 LINQ 查询一样继续操作:

public IEnumerable<InstructorName> GetInstructorNames()
{
    return (from i in context.InstructorNames orderby i.FullName select i).ToList();
}
public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    context.Departments.MergeOption = MergeOption.NoTracking;
    return (from d in context.Departments where d.Administrator == administrator select d).ToList();
}

可以将这些方法中的代码更改为上述代码,并运行应用程序以验证它是否正常工作,然后再继续操作。 但是,以下说明直接跳转到创建它们的预编译版本。

DAL 文件夹中创建一个类文件,将其命名为 SchoolEntities.cs,并将现有代码替换为以下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Objects;

namespace ContosoUniversity.DAL
{
    public partial class SchoolEntities
    {
        private static readonly Func<SchoolEntities, IQueryable<InstructorName>> compiledInstructorNamesQuery =
            CompiledQuery.Compile((SchoolEntities context) => from i in context.InstructorNames orderby i.FullName select i);

        public IEnumerable<InstructorName> CompiledInstructorNamesQuery()
        {
            return compiledInstructorNamesQuery(this).ToList();
        }

        private static readonly Func<SchoolEntities, Int32, IQueryable<Department>> compiledDepartmentsByAdministratorQuery =
            CompiledQuery.Compile((SchoolEntities context, Int32 administrator) => from d in context.Departments.Include("Person") where d.Administrator == administrator select d);

        public IEnumerable<Department> CompiledDepartmentsByAdministratorQuery(Int32 administrator)
        {
            return compiledDepartmentsByAdministratorQuery(this, administrator).ToList();
        }
    }
}

此代码创建一个分部类,用于扩展自动生成的对象上下文类。 分部类包括两个使用 Compile 类的 方法编译的 CompiledQuery LINQ 查询。 它还创建可用于调用查询的方法。 保存并关闭此文件。

接下来,在 SchoolRepository.cs 中,更改存储库类中的现有 GetInstructorNamesGetDepartmentsByAdministrator 方法,以便它们调用已编译的查询:

public IEnumerable<InstructorName> GetInstructorNames()
{
    return context.CompiledInstructorNamesQuery();
}
public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    return context.CompiledDepartmentsByAdministratorQuery(administrator);
}

运行 Departments.aspx 页,验证它是否像以前一样工作。 GetInstructorNames调用 方法是为了填充管理员下拉列表,当你GetDepartmentsByAdministrator单击“更新”以验证没有讲师是多个部门的管理员时,将调用 该方法。

Image03

你在 Contoso University 应用程序中预编译的查询只是为了了解如何执行此操作,而不是因为它会显著提高性能。 预编译 LINQ 查询确实会增加代码的复杂性,因此请确保仅针对实际表示应用程序中性能瓶颈的查询执行此操作。

检查发送到数据库的查询

调查性能问题时,有时了解实体框架要发送到数据库的确切 SQL 命令会很有帮助。 如果使用的是 IQueryable 对象,则执行此操作的一种方法是使用 ToTraceString 方法。

SchoolRepository.cs 中,更改 方法中的 GetDepartmentsByName 代码以匹配以下示例:

public IEnumerable<Department> GetDepartmentsByName(string sortExpression, string nameSearchString)
{
    ...
    var departments = new ObjectQuery<Department>("SELECT VALUE d FROM Departments AS d", context).OrderBy("it." + sortExpression).Include("Person").Include("Courses").Where(d => d.Name.Contains(nameSearchString));
    string commandText = ((ObjectQuery)departments).ToTraceString();
    return departments.ToList();
}

departments变量只能强制转换为类型,Where因为上一ObjectQuery行末尾的 方法创建对象IQueryable;如果没有 Where 方法,则不需要强制转换。

在行上 return 设置断点,然后在调试器中运行 Departments.aspx 页。 命中断点时,检查“局部变量”窗口中的commandText变量,并使用文本可视化工具 (“值”列中的放大镜) 在文本可视化工具窗口中显示其值。 可以看到此代码产生的整个 SQL 命令:

Image08

作为替代方法,Visual Studio Ultimate 中的 IntelliTrace 功能提供了一种查看实体框架生成的 SQL 命令的方法,这些命令不需要更改代码甚至设置断点。

注意

仅当Visual Studio Ultimate时,才能执行以下过程。

还原 方法中的 GetDepartmentsByName 原始代码,然后在调试器中运行 Departments.aspx 页。

在 Visual Studio 中,依次选择 “调试 ”菜单、 “IntelliTrace”和 “IntelliTrace 事件”。

Image11

IntelliTrace 窗口中,单击“ 全部中断”。

Image12

IntelliTrace 窗口显示最近发生的事件列表:

Image09

单击 ADO.NET 行。 展开它以显示命令文本:

Image10

可以从“ 局部变量 ”窗口将整个命令文本字符串复制到剪贴板。

假设你使用的数据库的表、关系和列数多于简单 School 数据库。 你可能会发现,在包含多个Join子句的单个Select语句中收集所需的所有信息的查询变得过于复杂,无法高效工作。 在这种情况下,可以从预先加载切换到显式加载,以简化查询。

例如,尝试在 SchoolRepository.cs 中更改 方法中的代码GetDepartmentsByName。 当前在该方法中,你有一个对象查询,其中包含 IncludeCourses 导航属性的方法Personreturn将 语句替换为执行显式加载的代码,如以下示例所示:

public IEnumerable<Department> GetDepartmentsByName(string sortExpression, string nameSearchString)
{
    ...
    var departments = new ObjectQuery<Department>("SELECT VALUE d FROM Departments AS d", context).OrderBy("it." + sortExpression).Where(d => d.Name.Contains(nameSearchString)).ToList();
    foreach (Department d in departments)
    {
        d.Courses.Load();
        d.PersonReference.Load();
    }
    return departments;
}

在调试器中运行 Departments.aspx 页,并像以前一样再次检查 IntelliTrace 窗口。 现在,以前有一个查询,你会看到一长串查询。

Image13

单击第一 ADO.NET 行,查看前面查看的复杂查询发生了什么情况。

Image14

来自 Departments 的查询已成为一个没有Join子句的简单Select查询,但它后面是单独的查询,用于检索相关课程和管理员,对原始查询返回的每个部门使用一组两个查询。

注意

如果启用延迟加载,则此处看到的模式(重复多次相同的查询)可能是迟缓加载造成的。 通常要避免的一种模式是为主表的每一行延迟加载相关数据。 除非你已验证单个联接查询太复杂而无法高效,否则通常可以通过将主查询更改为使用预先加载来提高在这种情况下的性能。

预生成视图

ObjectContext首次在新应用程序域中创建对象时,实体框架会生成一组用于访问数据库的类。 这些类称为 视图,如果具有非常大的数据模型,则生成这些视图可能会延迟网站在初始化新应用程序域后对页面的第一个请求的响应。 可以通过在编译时(而不是在运行时)创建视图来减少这种第一个请求延迟。

注意

如果应用程序没有非常大的数据模型,或者它确实具有大型数据模型,但你并不担心在回收 IIS 后仅影响第一页请求的性能问题,则可以跳过本部分。 视图创建不会在每次实例化 ObjectContext 对象时发生,因为视图缓存在应用程序域中。 因此,除非经常在 IIS 中回收应用程序,否则很少页面请求会受益于预生成的视图。

可以使用 EdmGen.exe 命令行工具或 文本模板转换工具包 (T4) 模板来预生成视图。 在本教程中,你将使用 T4 模板。

DAL 文件夹中,使用“文本模板”模板添加文件, (该文件位于“已安装模板”列表) 的“常规”节点下,并将其命名为 SchoolModel.Views.tt。 将 文件中的现有代码替换为以下代码:

<#
/***************************************************************************

Copyright (c) Microsoft Corporation. All rights reserved.

THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

***************************************************************************/
#>

<#
    //
    // TITLE: T4 template to generate views for an EDMX file in a C# project
    //
    // DESCRIPTION:
    // This is a T4 template to generate views in C# for an EDMX file in C# projects.
    // The generated views are automatically compiled into the project's output assembly.
    //
    // This template follows a simple file naming convention to determine the EDMX file to process:
    // - It assumes that [edmx-file-name].Views.tt will process and generate views for [edmx-file-name].EDMX
    // - The views are generated in the code behind file [edmx-file-name].Views.cs
    //
    // USAGE:
    // Do the following to generate views for an EDMX file (e.g. Model1.edmx) in a C# project
    // 1. In Solution Explorer, right-click the project node and choose "Add...Existing...Item" from the context menu
    // 2. Browse to and choose this .tt file to include it in the project 
    // 3. Ensure this .tt file is in the same directory as the EDMX file to process 
    // 4. In Solution Explorer, rename this .tt file to the form [edmx-file-name].Views.tt (e.g. Model1.Views.tt)
    // 5. In Solution Explorer, right-click Model1.Views.tt and choose "Run Custom Tool" to generate the views
    // 6. The views are generated in the code behind file Model1.Views.cs
    //
    // TIPS:
    // If you have multiple EDMX files in your project then make as many copies of this .tt file and rename appropriately
    // to pair each with each EDMX file.
    //
    // To generate views for all EDMX files in the solution, click the "Transform All Templates" button in the Solution Explorer toolbar
    // (its the rightmost button in the toolbar) 
    //
#>
<#
    //
    // T4 template code follows
    //
#>
<#@ template language="C#" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ output extension=".cs" #>
<# 
    // Find EDMX file to process: Model1.Views.tt generates views for Model1.EDMX
    string edmxFileName = Path.GetFileNameWithoutExtension(this.Host.TemplateFile).ToLowerInvariant().Replace(".views", "") + ".edmx";
    string edmxFilePath = Path.Combine(Path.GetDirectoryName(this.Host.TemplateFile), edmxFileName);
    if (File.Exists(edmxFilePath))
    {
        // Call helper class to generate pre-compiled views and write to output
        this.WriteLine(GenerateViews(edmxFilePath));
    }
    else
    {
        this.Error(String.Format("No views were generated. Cannot find file {0}. Ensure the project has an EDMX file and the file name of the .tt file is of the form [edmx-file-name].Views.tt", edmxFilePath));
    }
    
    // All done!
#>

<#+
    private String GenerateViews(string edmxFilePath)
    {
        MetadataLoader loader = new MetadataLoader(this);
        MetadataWorkspace workspace;
        if(!loader.TryLoadAllMetadata(edmxFilePath, out workspace))
        {
            this.Error("Error in the metadata");
            return String.Empty;
        }
            
        String generatedViews = String.Empty;
        try
        {
            using (StreamWriter writer = new StreamWriter(new MemoryStream()))
            {
                StorageMappingItemCollection mappingItems = (StorageMappingItemCollection)workspace.GetItemCollection(DataSpace.CSSpace);

                // Initialize the view generator to generate views in C#
                EntityViewGenerator viewGenerator = new EntityViewGenerator();
                viewGenerator.LanguageOption = LanguageOption.GenerateCSharpCode;
                IList<EdmSchemaError> errors = viewGenerator.GenerateViews(mappingItems, writer);

                foreach (EdmSchemaError e in errors)
                {
                    // log error
                    this.Error(e.Message);
                }

                MemoryStream memStream = writer.BaseStream as MemoryStream;
                generatedViews = Encoding.UTF8.GetString(memStream.ToArray());
            }
        }
        catch (Exception ex)
        {
            // log error
            this.Error(ex.ToString());
        }

        return generatedViews;
    }
#>

此代码为与模板位于同一文件夹中且与模板文件同名的 .edmx 文件生成视图。 例如,如果模板文件名为 SchoolModel.Views.tt,它将查找名为 SchoolModel.edmx 的数据模型文件。

保存文件,然后右键单击解决方案资源管理器中的文件,然后选择“运行自定义工具”。

Image02

Visual Studio 会生成一个代码文件,该文件基于模板创建名为 SchoolModel.Views.cs 的视图。 (你可能已经注意到,在选择 “运行自定义工具”之前,只要保存模板文件,就会生成代码文件。)

Image01

现在可以运行应用程序并验证它是否像以前一样工作。

有关预生成视图的详细信息,请参阅以下资源:

这完成了在使用实体框架的 ASP.NET Web 应用程序中提高性能的介绍。 有关更多信息,请参见以下资源:

下一篇教程回顾了版本 4 中新增的实体框架的一些重要增强功能。