在 ASP.NET 4 Web 應用程式中將 Entity Framework 4.0 的效能最大化

作者 :Tom Dykstra

本教學課程系列是以 Contoso University Web 應用程式為基礎,此應用程式是由使用Entity Framework 4.0教學課程系列消費者入門所建立。 如果您未完成先前的教學課程,作為本教學課程的起點,您可以下載您已建立 的應用程式 。 您也可以下載完整教學課程系列所建立 的應用程式 。 如果您有關于教學課程的問題,您可以將這些教學課程張貼到 ASP.NET Entity Framework 論壇

在上一個教學課程中,您已瞭解如何處理並行衝突。 本教學課程說明如何改善使用 Entity Framework ASP.NET Web 應用程式的效能的選項。 您將瞭解數種方法,以將效能最大化或診斷效能問題。

下列各節中提供的資訊在各種案例中可能很有用:

  • 有效率地載入相關資料。
  • 管理檢視狀態。

如果您有個別查詢呈現效能問題,下列各節中提供的資訊可能很有用:

  • NoTracking使用合併選項。
  • 預先編譯的 LINQ 查詢。
  • 檢查傳送至資料庫的查詢命令。

下一節中提供的資訊對於具有極大型資料模型的應用程式可能很有用:

  • 預先產生檢視。

注意

Web 應用程式效能會受到許多因素的影響,包括要求和回應資料的大小、資料庫查詢的速度、伺服器可以排入佇列的要求數目,以及服務要求的速度,甚至是您可能使用的任何用戶端腳本程式庫效率。 如果您的應用程式中效能很重要,或測試或體驗顯示應用程式效能不滿意,您應該遵循一般通訊協定來進行效能調整。 測量以判斷發生效能瓶頸的位置,然後解決會對整體應用程式效能造成最大影響的區域。

本主題主要著重于您可以在 ASP.NET 中特別改善 Entity Framework 效能的方式。 如果您判斷資料存取是應用程式中的其中一個效能瓶頸,這裡的建議會很有用。 除非另有說明,此處說明的方法通常不應視為「最佳做法」,其中許多方法僅適用于例外狀況或解決非常特定的效能瓶頸。

若要開始本教學課程,請啟動 Visual Studio,然後開啟您在上一個教學課程中使用的 Contoso University Web 應用程式。

Entity Framework 有數種方式可將相關資料載入實體的導覽屬性:

  • 延遲載入。 第一次讀取實體時,不會擷取相關資料。 不過,第一次嘗試存取導覽屬性時,將會自動擷取該導覽屬性所需的資料。 這會導致傳送至資料庫的多個查詢, 一個用於實體本身,每次必須擷取實體的相關資料時,一個查詢。

    Image05

積極式載入。 讀取實體時,將會同時擷取其相關資料。 這通常會導致單一聯結查詢,其可擷取所有需要的資料。 您可以使用 方法來指定積極式載入 Include ,如您已在這些教學課程中所見。

Image07

  • 明確載入。 這類似于延遲載入,不同之處在于您明確擷取程式碼中的相關資料;當您存取導覽屬性時,它不會自動發生。 您可以使用集合導覽屬性的 方法手動 Load 載入相關資料,或使用 Load 保存單一物件之屬性之參考屬性的 方法。 (例如,您可以呼叫 PersonReference.Load 方法來載入 Person entity 的 Department 導覽屬性。)

    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 網頁必須在轉譯頁面時儲存實體的原始屬性值。 在回傳處理期間,控制項可以重新建立實體的原始狀態,並在套用變更並呼叫 SaveChanges 方法之前呼叫實體的 Attach 方法。 根據預設,ASP.NET Web Forms資料控制項會使用檢視狀態來儲存原始值。 不過,檢視狀態可能會影響效能,因為它儲存在隱藏的欄位中,可大幅增加傳送至瀏覽器和從瀏覽器傳送的頁面大小。

管理檢視狀態的技術或會話狀態之類的替代專案不是 Entity Framework 特有的技術,因此本教學課程不會詳細說明本主題。 如需詳細資訊,請參閱教學課程結尾的連結。

不過,第 4 版 ASP.NET 提供使用檢視狀態的新方式,讓Web Form應用程式的每個 ASP.NET 開發人員都應該注意: ViewStateMode 屬性。 這個新屬性可以在頁面或控制項層級設定,而且可讓您預設停用頁面的檢視狀態,並只針對需要它的控制項啟用它。

對於效能很重要的應用程式,最佳做法是一律停用頁面層級的檢視狀態,並只針對需要它的控制項加以啟用。 此方法不會大幅減少 Contoso University 頁面中的檢視狀態大小,但若要查看其運作方式,您將針對 Instructors.aspx 頁面執行。 該頁面包含許多控制項,包括 Label 已停用檢視狀態的控制項。 此頁面上的控制項實際上都不需要啟用檢視狀態。 (控制項 DataKeyNamesGridView 屬性會指定必須在回傳之間維護的狀態,但這些值會保留在控制項狀態中,這不會受到 ViewStateMode property 的影響。)

指示 Page 詞和 Label 控制項標記目前類似下列範例:

<%@ 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 指示詞。
  • Label 控制項中移除 ViewStateMode="Disabled"

標記現在類似下列範例:

<%@ 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 應用程式通常有短期的物件內容實例,所以查詢通常會傳回不需要追蹤的資料,因為讀取它們的物件內容將會在再次使用或更新其讀取的任何實體之前處置。

在 Entity Framework 中,您可以藉由設定 合併選項來指定物件內容是否追蹤實體物件。 您可以為個別查詢或實體集設定合併選項。 如果您針對實體集設定它,這表示您要針對針對該實體集建立的所有查詢設定預設合併選項。

針對 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 查詢

第一次 Entity Framework 在指定 ObjectContext 實例的生命週期內執行 Entity 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 查詢會為您的程式碼增加複雜度層級,因此請確定您只針對實際代表應用程式中效能瓶頸的查詢執行。

檢查傳送至資料庫的查詢

當您調查效能問題時,有時知道 Entity Framework 傳送至資料庫的確切 SQL 命令會很有説明。 如果您使用 IQueryable 物件,其中一種方法是使用 ToTraceString 方法。

SchoolRepository.csGetDepartmentsByName ,變更 方法中的程式碼,以符合下列範例:

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 只能轉換成 ObjectQuery 類型, Where 因為前一行結尾的 方法會建立 IQueryable 物件;如果沒有 Where 方法,則不需要轉換。

在行上 return 設定中斷點,然後在偵錯工具中執行 Departments.aspx 頁面。 當您叫用中斷點時,請檢查 commandText[區域變數 ] 視窗中的變數,並使用文字視覺化檢視 ([ ] 資料行中的放大鏡,) 在 [文字視覺化檢 視] 視窗中顯示其值。 您可以看到此程式碼所產生的整個 SQL 命令:

Image08

或者,Visual Studio Ultimate中的 IntelliTrace 功能提供一種方式來檢視 Entity Framework 所產生的 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第一次在新的應用程式域中建立物件時,Entity Framework 會產生一組用來存取資料庫的類別。 這些類別稱為 檢視,而且如果您有非常大的資料模型,產生這些檢視可能會延遲網站在初始化新的應用程式域之後,對頁面第一個要求的回應。 您可以藉由在編譯時間建立檢視,而不是在執行時間建立檢視,以減少此第一個要求延遲。

注意

如果您的應用程式沒有極大型的資料模型,或它確實有大型資料模型,但您不擔心只影響 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

您現在可以執行應用程式,並確認它如先前一樣運作。

如需預先產生之檢視的詳細資訊,請參閱下列資源:

這會完成使用 Entity Framework 之 ASP.NET Web 應用程式中改善效能的簡介。 如需詳細資訊,請參閱下列資源:

下一個教學課程會檢閱第 4 版中新版 Entity Framework 的一些重要增強功能。