實體框架 5 性能注意事項
作者:David Obando、Eric Dettinger 等
發布時間:2012 年 4 月
1.簡介
對象關系映射框架是一種在面向對象的應用程序中提供數據訪問抽象的便捷方式。對於 .NET 應用程序,Microsoft 推薦的 O/RM 是實體框架。但任何抽象都要考慮性能。
本白皮書旨在介紹在使用實體框架開發應用程序時的性能注意事項,使開發人員了解能夠影響性能的實體框架內部算法,以及提供有關進行調查及在使用實體框架的應用程序中提高性能的提示。網絡上有大量很好的有關性能的主題,我們還盡可能地指出這些資源的鏈接。
性能是一個很微妙的主題。對於使用實體框架的應用程序,可將本白皮書作為資源來幫助做出與性能相關的決策。我們提供了一些測試指標來演示性能,但這些指標不是在應用程序中看到的性能的絕對指標。
出於實用考慮,本文假設實體框架 4 在 .NET 4.0 下運行,實體框架 5 在 .NET 4.5 下運行。對實體框架 5 進行的許多性能改進存在於 .NET 4.5 附帶的核心組件中。
2.冷與熱查詢執行
第一次針對給定模型進行任何查詢時,實體框架在后台進行了大量工作來加載和驗證模型。我們經常將這個第一次查詢稱為“冷”查詢。針對已加載模型的進一步查詢稱為“熱”查詢,速度更快。
我們深入了解一下在使用實體框架執行查詢時,時間花在了哪里,看看實體框架 5 在哪些方面進行了改進。
首次查詢執行 — 冷查詢
第二次查詢執行 — 熱查詢
有幾種方式可降低冷、熱查詢的性能成本,后面幾節將探討這些方式。具體講,我們將介紹通過使用預生成的視圖降低冷查詢中模型加載的成本,這應有助於緩解在視圖生成過程中遇到的性能問題。對於熱查詢,將介紹查詢計划緩存、無跟蹤查詢和不同的查詢執行選項。
2.1 什么是視圖生成?
要了解什么是視圖生成,必須先了解什么是“映射視圖”。映射視圖是每個實體集和關聯的映射中指定的轉換的可執行表示。在內部,這些映射視圖采用 CQT(規范查詢樹)的形狀。映射視圖有兩種類型:
- 查詢視圖:表示從數據庫架構轉到概念架構所需的規范轉換。
- 更新視圖:表示從概念模型轉到數據庫架構所需的規范轉換。
根據映射規范計算這些視圖的過程即是所謂的視圖生成。視圖生成可在加載模型時動態進行,也可在生成時通過使用“預生成的視圖”進行;后者以實體 SQL 語句的形式序列化為 C# 或 VB 文件。
生成視圖時還會對它們進行驗證。從性能角度看,視圖生成的絕大部分成本實際上是視圖驗證產生的,視圖驗證可確保實體之間的連接有意義,並且對於所有支持的操作都有正確的基數。
在執行實體集查詢時,查詢與相應查詢視圖相組合,這種組合的結果通過計划編譯器運行,以便創建后備存儲能夠理解的查詢表示。對於 SQL Server,編譯的最終結果是 T-SQL SELECT 語句。首次對實體集執行更新時,更新視圖通過類似過程運行,將其轉換成用於目標數據庫的 DML 語句。
2.2 影響視圖生成性能的因素
視圖生成步驟的性能不僅取決於模型的大小,還取決於模型的互連方式。如果兩個實體通過繼承鏈或關聯進行連接,則稱它們已連接。同樣,如果兩個表通過外鍵進行連接,則它們已連接。隨着架構中已連接實體和表數目的增加,視圖生成的成本也增加。
在最糟糕的情況下,盡管我們使用一些優化進行改進,用於生成和驗證視圖的算法仍呈現指數特性。對性能具有最大負面影響的因素有:
- 模型大小,指的是實體的數目以及這些實體之間的關聯數量。
- 模型復雜性,具體講是涉及大量類型的繼承。
- 使用獨立關聯,而不是外鍵關聯。
簡單的小模型,成本小到不值得使用預生成的視圖。隨着模型大小和復雜性的增加,有多種選擇可降低視圖生成和驗證的成本。
2.3 使用預生成的視圖縮短模型加載時間
2.3.1 如何在 EDMGen 創建的模型中使用預生成的視圖
當利用 EDMGen 生成模型時,輸出包含一個 Views 文件。這是一個代碼文件,包含針對每個實體集的實體 SQL 代碼段。要啟用預生成的視圖,在項目中包含此文件即可。
如果手動編輯模型的架構文件,需要重新生成視圖文件。為此,可帶 /mode:ViewGeneration 標志運行 EDMGen。
有關進一步參考,請參見 MSDN 主題“如何:預生成視圖以改善查詢性能”: http://msdn.microsoft.com/library/bb896240.aspx。
2.3.2 如何使用 EDMX 文件的預生成視圖
另外,還可以使用 EDMGen 來生成 EDMX 文件的視圖 — 前面提到的 MSDN 主題介紹如何添加預生成事件來執行此操作 — 但這很復雜,並且有時候不適用。當模型位於 edmx 文件中時,使用 T4 模板生成視圖通常更加容易。
ADO.NET 團隊博客中有一篇文章介紹如何使用 T4 模板進行視圖生成 ( http://blogs.msdn.com/b/adonet/archive/2008/06/20/how-to-use-a-t4-template-for-view-generation.aspx)。這篇文章包括一個模板,您可以下載和添加到項目中。這個模板是為實體框架的第一個版本編寫的。要在 Visual Studio 2010 中使用這個模板,需要在 GetConceptualMappingAndStorageReaders 方法中修改 XML 命名空間,以便使用實體框架 5 的命名空間:
XNamespace edmxns = "http://schemas.microsoft.com/ado/2009/11/edmx";
XNamespace csdlns = "http://schemas.microsoft.com/ado/2009/11/edm";
XNamespace mslns = "http://schemas.microsoft.com/ado/2009/11/mapping/cs";
XNamespace ssdlns = "http://schemas.microsoft.com/ado/2009/11/edm/ssdl";
2.3.3 如何在 Code First 模型中使用預生成的視圖
另外,可以在 Code First 項目中使用預生成的視圖。實體框架 Power Tools 能夠為 Code First 項目生成視圖文件。通過在 Visual Studio 庫中搜索“實體框架 Power Tools”,可以找到這些增強工具。在編寫本文時,預發行版 CTP1 中提供了增強工具。
2.4 降低視圖生成的成本
使用預生成的視圖可將視圖生成成本從模型加載(運行時)轉移到編譯時。盡管這會改善運行時的啟動性能,但在開發時仍會遇到視圖生成問題。有幾種其他技巧可幫助在編譯時和運行時降低視圖生成的成本。
2.4.1 使用外鍵關聯降低視圖生成成本
將模型中的關聯從獨立關聯轉換為外鍵關聯可極大縮短視圖生成所用的時間,這種情況很常見。
為演示這種改進,我們使用 EDMGen 生成了 Navision 模型的兩個版本。注意:有關 Navision 模型的說明,請參見附錄 C。在這個練習中,Navision 模型非常有趣,它有大量實體,實體之間有大量關系。
這種超大模型的一個版本是使用外鍵關聯生成的,另一個是使用獨立關聯生成的。然后我們對使用 EDMGen 為每個模型生成視圖所用的時間進行了計時。對於使用外鍵的模型,視圖生成所用的時間為 104 分鍾。不知道生成使用獨立關聯的模型會用多長時間。我們讓這次測試運行了一個多月,然后在實驗室中重新啟動計算機,以便安裝每月更新。
2.4.1.1 如何使用外鍵,而不是獨立關聯
當使用 EDMGen 或 Visual Studio 中的 Entity Designer 時,在默認情況下會得到外鍵,它僅用一個復選框或命令行標志在外鍵與獨立關聯之間進行切換。
如果有大型 Code First 模型,使用獨立關聯對視圖生成具有相同影響。可通過在依賴對象的類上包含外鍵屬性來避免這種影響,但有些開發人員認為這會污染他們的對象模型。在 http://blog.oneunicorn.com/2011/12/11/whats-the-deal-with-mapping-foreign-keys-using-the-entity-framework/ 中可獲得有關該主題的更多信息。
使用的工具 | 進行的操作 |
實體設計器 | 在兩個實體之間添加了關聯后,確保具有引用約束。引用約束告訴實體框架使用外鍵,而不是獨立關聯。有關更多詳細信息,請訪問 http://blogs.msdn.com/b/efdesign/archive/2009/03/16/foreign-keys-in-the-entity-framework.aspx。 |
EDMGen | 使用 EDMGen 從數據庫生成文件時,需要外鍵,因此外鍵會添加到模型中。有關 EDMGen 公開的不同選項的更多信息,請訪問 http://msdn.microsoft.com/library/bb387165.aspx。 |
Code First | 有關在使用 Code First 時如何包含依賴對象的外鍵屬性的信息,請參見 MSDN 中“Code First 約定”主題的“關系約定”部分 ( http://msdn.microsoft.com/library/hh161541(v=VS.103).aspx)。 |
2.4.2 將模型移到單獨程序集
如果在應用程序的項目中直接包含模型,通過預生成事件或 T4 模板生成視圖,則只要重新生成項目,即使沒有更改模型,也會進行視圖生成和驗證。如果將模型移到單獨程序集,從應用程序的項目中引用它,則可對應用程序進行其他更改,無需重新生成包含模型的項目。
注意:在將模型移到單獨程序集時,記住將模型的連接字符串復制到客戶端項目的應用程序配置文件中。
2.4.3 禁用對基於 edmx 的模型的驗證
EDMX 模型在編譯時進行驗證,即使模型未更改也是如此。如果已經驗證了模型,則可通過在屬性窗口中將“生成時驗證”屬性設置為 False 來禁用驗證。更改映射或模型時,可臨時重新啟用驗證,以驗證更改。
2.4.4 將模型標記為只讀
如果應用程序僅用於查詢方案,則可通過向 XML 映射中的 EntityContainerMapping 元素添加 GenerateUpdateViews 屬性,然后將其設置為 False,將模型標記為只讀。經驗表明,生成更新視圖的成本比生成查詢視圖的成本更高,因此要意識到這一點,避免在不需要時生成更新視圖。
3實體框架中的緩存
實體框架有以下內置緩存形式:
- 對象緩存 — 內置在 ObjectContext 實例中的 ObjectStateManager 保持跟蹤,以便記住已使用該實例檢索的對象。這也稱為一級緩存。
- 查詢計划緩存 — 在多次執行查詢時重用生成的存儲命令。
- 元數據緩存 — 在與同一模型的不同連接之間共享模型的元數據。
除實體框架提供的隨取即用緩存外,還可使用一種特殊類型的 ADO.NET 數據提供程序(稱為包裝提供程序)來擴展實體框架,使其能夠緩存從數據庫中檢索的結果,這也稱為二級緩存。
3.1 對象緩存
在默認情況下,當查詢結果中返回一個實體時,在 EF 剛對它進行具體化前,ObjectContext 將檢查是否已經將具有相同鍵的實體加載到了其 ObjectStateManager 中。如果已經存在具有相同鍵的實體,則實體框架會將其包含在查詢結果中。盡管 EF 仍將發出對數據庫的查詢,但此行為可避免多次具體化該實體的大部分成本。
3.1.1 使用 DbContext Find 從對象緩存中獲得實體
與常規查詢不同,DbSet(API 首次包含在 EF 4.1 中)中的 Find 方法將在內存中執行搜索,即使在發出對數據庫的查詢之前也是如此。注意,兩個不同的 ObjectContext 實例將具有兩個不同的 ObjectStateManager 實例,這一點非常重要,這意味着它們有單獨的對象緩存。
Find 使用主鍵值嘗試查找上下文所跟蹤的實體。如果該實體沒有在上下文中,則執行和評估對數據庫的查詢,如果在上下文或數據庫中沒有發現該實體,則返回 null。注意,Find 還返回已添加到上下文但尚未保存到數據庫中的實體。
使用 Find 時,有一項性能注意事項。在默認情況下,對此方法的調用將觸發對象緩存的驗證,以便檢測仍在等待提交到數據庫的更改。如果對象緩存中或者要添加到對象緩存的大型對象圖中有非常多的對象,則此過程的成本可能會非常高,但也可禁用此過程。在某些情況下,當禁用自動檢測更改時,在調用 Find 方法方面可能存在巨大差異。對象實際上位於緩存中與必須從數據庫檢索對象這兩種情況,也存在巨大差異。以下是使用我們的一些微基准進行測量的示例圖,單位為毫秒,加載了 5000 個實例:
自動檢測更改已禁用的 Find 示例:
context.Configuration.AutoDetectChangesEnabled = false;
var product = context.Products.Find(productId);
...
使用 Find 方法時必須考慮:
- 如果對象沒有在緩存中,則 Find 沒有優勢,但語法仍比按鍵進行查詢簡單。
- 如果啟用自動檢測更改,則根據模型的復雜性以及對象緩存中的實體數量,Find 方法的成本可能會增加一個數量級,甚至更多。
此外,請注意 Find 僅返回要查找的實體,它不會自動加載未在對象緩存中的關聯實體。如果需要檢索關聯實體,可通過預先加載使用按鍵查詢。
3.1.2 當對象緩存具有許多實體時的性能問題
對象緩存有助於提高實體框架的整體響應能力。但當對象緩存中加載了大量實體時,可能影響某些操作,例如添加、刪除、SaveChanges 等。尤其是,極大的對象緩存將對觸發對 DetectChanges 的調用的操作產生負面影響。DetectChanges 將對象圖與對象狀態管理器進行同步,其性能將直接取決於對象圖的大小。有關 DetectChanges 的更多信息,請參見 http://msdn.microsoft.com/library/dd456848.aspx。
3.2 查詢計划緩存
查詢首次執行時,通過內部計划編譯器將概念查詢轉換為存儲命令(例如當針對 SQL Server 運行時執行的 T-SQL)。如果啟用了查詢計划緩存,則在下一次執行此查詢時,將直接從查詢計划緩存中檢索存儲命令,以便執行,從而繞開計划編譯器。
同一 AppDomain 中的 ObjectContext 實例間共享查詢計划緩存。要利用查詢計划緩存,不一定只使用一個 ObjectContext 實例。
3.2.1 有關查詢計划緩存的一些注意事項
- 所有查詢類型均共享查詢計划緩存:實體 SQL、LINQ 及 CompiledQuery 對象。
- 對於實體 SQL 查詢,無論是通過 EntityCommand 還是通過 ObjectQuery 執行,在默認情況下都會啟用查詢計划緩存。在 EF 5.0 中,默認情況下對 LINQ 查詢也會啟用。
- 通過將 EnablePlanCaching 屬性(在 EntityCommand 或 ObjectQuery 上)設置為 False,可以禁用查詢計划緩存。
- 對於參數化查詢,更改參數值仍將命中已緩存的查詢。但更改參數的方面(例如大小、精確度或數值范圍)將命中緩存中的其他實例。
- 當使用實體 SQL 時,查詢字符串是鍵的一部分。完全更改查詢將產生不同的緩存條目,即使這些查詢具有等同的功能也是如此。這包括對大小寫或空格的更改。
- 當使用 LINQ 時,將對查詢進行處理,以生成鍵的一部分。因此更改 LINQ 表達式將生成不同的鍵。
- 可能存在其他技術限制;有關更多詳細信息,請參見自動編譯的查詢。
3.2.2 緩存逐出算法
了解內部算法的工作方式有助於確定何時啟用或禁用查詢計划緩存。清除算法如下:
- 在緩存包含設定數目 (800) 的條目后,我們啟動定期(每分鍾一次)整理緩存的計時器。
- 在緩存整理過程中,將根據 LFRU(最不常使用 - 最近使用)刪除緩存中的條目。在確定彈出哪些條目時,此算法同時考慮命中次數和期限。
- 在每次緩存整理結束時,緩存會再次包含 800 個條目。
在確定逐出哪些條目時會公平對待所有緩存條目。這意味着針對 CompiledQuery 的存儲命令與針對實體 SQL 查詢的存儲命令具有相同的逐出幾率。
3.2.3 演示查詢計划緩存性能的測試指標
為演示查詢計划緩存對應用程序性能的影響,我們進行了一項測試,在測試中,我們對 Navision 模型執行了大量實體 SQL 查詢。有關 Navision 模型的說明以及執行的查詢類型,請參見附錄。在該測試中,我們首先循環訪問查詢列表,對每個查詢執行一次,將它們添加到緩存中(如果緩存已啟用)。此步驟不計時。下一步,再次循環訪問列表,執行緩存的查詢。
3.2.3.1 測試結果
測試 | 緩存已啟用? | 結果 |
枚舉所有 18723 個查詢 | 否 | 所用秒數=238.14 |
是 | 所用秒數=240.31 | |
避免整理(無論復雜性如何,僅前 800 個查詢) | 否 | 所用秒數=61.62 |
是 | 所用秒數=0.84 | |
僅 AggregatingSubtotals 查詢(共 178 個 - 避免整理) | 否 | 所用秒數=63.22 |
是 | 所用秒數=0.41 |
道理 - 當執行許多不同的查詢(例如,動態創建的查詢)時,緩存沒有幫助,並且最終的緩存刷新會使最能受益於計划緩存的查詢實際上無法使用它。
AggregatingSubtotals 查詢是我們測試的最復雜的查詢。像預計的那樣,查詢越復雜,越能受益於查詢計划緩存。
因為 CompiledQuery 實際上是緩存了計划的 LINQ 查詢,所以 CompiledQuery 與等同實體 SQL 查詢的比較應具有類似結果。實際上,如果應用程序有許多動態實體 SQL 查詢,向緩存中填充查詢還會在從緩存中刷新查詢時使 CompiledQueries 進行“反編譯”。在這種情況下,通過禁用動態查詢緩存來確定 CompiledQueries 優先級,可以提高性能。當然,最好將應用程序重新編寫為使用參數化查詢,而不是動態查詢。
3.3 使用 CompiledQuery 改善 LINQ 查詢的性能
我們的測試表明,比起自動編譯的 LINQ 查詢,使用 CompiledQuery 可以帶來 7% 的益處;這意味着從實體框架堆棧執行代碼將節省 7% 的時間;這不意味着應用程序的速度將提高 7%。一般而言,與獲得的好處相比,在 EF 5.0 中編寫和維護 CompiledQuery 對象的成本是不值當的。實際情況可能各有不同,因此如果項目需要額外推動力,則運用這種方法。
有關創建和調用 CompiledQuery 的更多信息,請參見 MSDN 文檔中的“已編譯查詢 (LINQ to Entities)”主題:http://msdn.microsoft.com/library/bb896297.aspx。
使用 CompiledQuery 時有兩個注意事項,即,使用靜態實例的要求,以及它們具有的可組合性要求。以下是這兩個注意事項的深入說明。
3.3.1 使用靜態 CompiledQuery 實例
由於編譯 LINQ 查詢是一個非常耗時的過程,我們不想每次從數據庫中提取數據時都執行此過程。通過 CompiledQuery 實例,可以編譯一次后運行多次,但您必須仔細,每次重用相同 CompiledQuery 實例,而不是一遍一遍地編譯它。必須使用靜態成員存儲 CompiledQuery 實例;否則沒有任何用處。
例如,假設頁面用以下方法主體處理顯示所選類別的產品:
// 警告:這是錯誤使用 CompiledQuery 的方式
using (NorthwindEntities context = new NorthwindEntities())
{
string selectedCategory = this.categoriesList.SelectedValue;
var productsForCategory = CompiledQuery.Compile<NorthwindEntities, string, IQueryable<Product>>(
(NorthwindEntities nwnd, string category) =>
nwnd.Products.Where(p => p.Category.CategoryName == category)
);
this.productsGrid.DataSource = productsForCategory.Invoke(context, selectedCategory).ToList();
this.productsGrid.DataBind();
}
this.productsGrid.Visible = true;
在這種情況下,每次調用此方法時都會實時創建一個新的 CompiledQuery 實例。每次創建新實例時 CompiledQuery 都會經過計划編譯器,而不是通過從查詢計划緩存中檢索存儲命令來獲得性能優勢。實際上,每次調用此方法時,新的 CompiledQuery 條目都會污染查詢計划緩存。
您需要創建已編譯查詢的靜態實例,因此每次調用此方法時都在調用相同的已編譯查詢。為此,一種方法是添加 CompiledQuery 實例作為對象上下文的成員。然后可通過幫助程序方法訪問 CompiledQuery,這樣更簡單一些:
public partial class NorthwindEntities : ObjectContext
{
private static readonly Func<NorthwindEntities, string, IEnumerable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
(NorthwindEntities context, string categoryName) =>
context.Products.Where(p => p.Category.CategoryName == categoryName)
);
public IEnumerable<Product> GetProductsForCategory(string categoryName)
{
return productsForCategoryCQ.Invoke(this, categoryName).ToList();
}
此幫助程序方法將按照以下方式加以調用:
this.productsGrid.DataSource = context.GetProductsForCategory(selectedCategory);
3.3.2 在 CompiledQuery 上編寫
在任何 LINQ 查詢上進行編寫的能力非常有用;為此,只需在 IQueryable 后調用一個方法,例如 Skip() 或 Count()。但這樣做實際上會返回一個新的 IQueryable 對象。盡管沒有什么能夠在技術上阻止您在 CompiledQuery 上進行編寫,但這樣做會生成需要再次通過計划編譯器的新 IQueryable 對象。
某些組件將利用所編寫的 IQueryable 對象啟用高級功能。例如,可通過 SelectMethod 屬性將 ASP.NET 的 GridView 數據綁定到 IQueryable 對象。然后 GridView 將在該 IQueryable 對象上進行撰寫,以便允許在數據模型上進行排序和分頁。可以看到,將 CompiledQuery 用於 GridView 不會命中已編譯查詢,但會生成新的自動編譯查詢。
客戶顧問團隊在他們的“已編譯 LINQ 查詢重新編譯的潛在性能問題”博客文章中探討了這一方面:http://blogs.msdn.com/b/appfabriccat/archive/2010/08/06/potential-performance-issues-with-compiled-linq-query-re-compiles.aspx。
在將漸進式篩選器添加到查詢中時可能會遇到此問題。例如,假設客戶頁面有針對可選篩選器(例如 Country 和 OrdersCount)的多個下拉列表。可針對 CompiledQuery 的 IQueryable 結果編寫這些篩選器,但這樣會導致每次執行新查詢時,新查詢都會經過計划編譯器。
using (NorthwindEntities context = new NorthwindEntities())
{
IQueryable<Customer> myCustomers = context.InvokeCustomersForEmployee();
if (this.orderCountFilterList.SelectedItem.Value != defaultFilterText)
{
int orderCount = int.Parse(orderCountFilterList.SelectedValue);
myCustomers = myCustomers.Where(c => c.Orders.Count > orderCount);
}
if (this.countryFilterList.SelectedItem.Value != defaultFilterText)
{
myCustomers = myCustomers.Where(c => c.Address.Country == countryFilterList.SelectedValue);
}
this.customersGrid.DataSource = myCustomers;
this.customersGrid.DataBind();
}
為避免這種重復編譯,可重寫 CompiledQuery,以便考慮可能的篩選器:
private static readonly Func<NorthwindEntities, int, int?, string, IQueryable<Customer>> customersForEmployeeWithFiltersCQ = CompiledQuery.Compile(
(NorthwindEntities context, int empId, int? countFilter, string countryFilter) =>
context.Customers.Where(c => c.Orders.Any(o => o.EmployeeID == empId))
.Where(c => countFilter.HasValue == false || c.Orders.Count > countFilter)
.Where(c => countryFilter == null || c.Address.Country == countryFilter)
);
這將在 UI 中調用,例如:
using (NorthwindEntities context = new NorthwindEntities())
{
int? countFilter = (this.orderCountFilterList.SelectedIndex == 0) ?
(int?)null :
int.Parse(this.orderCountFilterList.SelectedValue);
string countryFilter = (this.countryFilterList.SelectedIndex == 0) ?
null :
this.countryFilterList.SelectedValue;
IQueryable<Customer> myCustomers = context.InvokeCustomersForEmployeeWithFilters(
countFilter, countryFilter);
this.customersGrid.DataSource = myCustomers;
this.customersGrid.DataBind();
}
此處需要權衡的是,生成的存儲命令將始終具有帶 null 檢查的篩選器,但這些應可使數據庫服務器比較簡單地進行優化:
...
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 元數據緩存
實體框架還支持元數據緩存。這實質上是與同一模型的不同連接之間的類型信息以及類型到數據庫映射信息的緩存。元數據緩存每 AppDomain 都是唯一的。
3.4.1 元數據緩存算法
- 模型的元數據信息存儲在每個 EntityConnection 的 ItemCollection 中。
- 順便說一下,模型的不同部分有不同的 ItemCollection 對象,例如 StoreItemCollections 包含有關數據庫模型的信息;ObjectItemCollection 包含有關數據模型的信息;EdmItemCollection 包含有關概念模型的信息。
- 如果兩個連接使用同一連接字符串,則它們將共享同一 ItemCollection 實例。
- 功能等同但文本不同的連接字符串可產生不同的元數據緩存。我們標記了連接字符串,因此僅更改這些標記的順序應產生共享元數據。但是,標記之后看起來功能相同的兩個連接字符串不能評估為同一連接字符串。
- 定期檢查 ItemCollection 是否可用。如果確定最近尚未訪問工作區,則將對其進行標記,以便在下一次緩存整理時進行清除。
- 僅創建 EntityConnection 會導致創建元數據緩存(盡管直到打開連接才會初始化其中的項集合)。此工作區將保持在內存中,直到緩存算法確定它未“在使用”為止。
客戶顧問團隊寫了一篇博客文章,介紹如何保留對 ItemCollection 的引用,以便在使用大模型時避免“不推薦使用的情況”:http://blogs.msdn.com/b/appfabriccat/archive/2010/10/22/metadataworkspace-reference-in-wcf-services.aspx。
3.4.2 元數據緩存與查詢計划緩存之間的關系
查詢計划緩存實例存在於 MetadataWorkspace 的存儲類型 ItemCollection 中。這意味着緩存的存儲命令將用於對參照給定 MetadataWorkspace 進行了實例化的任何 ObjectContext 的查詢。這還意味着,如果有兩個略微不同且在標記之后不匹配的連接字符串,將有不同的查詢計划緩存實例。
3.5 結果緩存
憑借結果緩存(也稱為“二級緩存”),可將查詢結果保留在本地緩存中。在發布查詢時,首先查看在對存儲進行查詢前是否可本地獲得這些結果。盡管實體框架不直接支持結果緩存,但可通過使用包裝提供程序添加二級緩存。CodePlex 上提供了具有二級緩存的包裝提供程序示例:http://code.msdn.microsoft.com/EFProviderWrappers-c0b88f32/view/Discussions/2。
3.5.1 包裝提供程序結果緩存的其他參考
- Julie Lerman 寫過一篇題為“實體框架和 Windows Azure 中的二級緩存”的 MSDN 文章,說明如何更新示例包裝提供程序,以使用 Windows Server AppFabric 緩存: http://msdn.microsoft.com/magazine/hh394143.aspx
- 實體框架團隊博客中有一篇文章,介紹如何通過緩存提供程序實現運行: http://blogs.msdn.com/b/adonet/archive/2010/09/13/ef-caching-with-jarek-kowalski-s-provider.aspx。這篇文章還包含一個 T4 模板,可用於自動將二級緩存添加到項目中。
4 自動編譯的查詢
當使用實體框架發出數據庫查詢時,在實際具體化結果之前必須經歷一系列步驟;其中一個步驟是查詢編譯。已知實體 SQL 查詢具有很好的性能,因為它們是自動緩存的,因此在第二次或第三次執行同一查詢時,可跳過計划編譯器,而使用緩存的計划。
實體框架 5 還引入了對 LINQ to Entities 的自動緩存。在實體框架的過去版本中,通過創建 CompiledQuery 來提高性能是一種常見做法,因為這會使 LINQ to Entities 查詢可緩存。由於現在緩存是自動進行的,無需使用 CompiledQuery,因此我們將該功能稱為“自動編譯的查詢”。有關查詢計划緩存及其機制的更多信息,請參見查詢計划緩存。
實體框架檢測查詢何時需要重新編譯,在調用查詢時,即使之前已對其進行了編譯,也會對其進行重新編譯。導致重新編譯查詢的常見條件是:
- 更改與查詢關聯的 MergeOption。將不使用緩存的查詢,而是再次運行計划編譯器,並且緩存新創建的計划。
- 更改 ContextOptions.UseCSharpNullComparisonBehavior 的值。這會獲得與更改 MergeOption 相同的效果。
其他條件可能阻礙查詢使用緩存。常見示例為:
- 使用 IEnumerable<T>.Contains<>(T value)
- 將查詢與需要重新編譯的另一個查詢鏈接起來。
4.1 使用 IEnumerable<T>.Contains<T>(T value)
實體框架不緩存調用 IEnumerable<T>.Contains<T>(T value) 的對內存中集合的查詢,因為該集合的值不穩定。以下示例查詢不緩存,因此將始終由計划編譯器加以處理:
int[] ids = new int[10000];
...
using (var context = new MyContext())
{
var query = context.MyEntities
.Where(entity => ids.Contains(entity.Id));
var results = query.ToList();
...
}
此外請注意,執行 Contains 所針對的 IEnumerable 的大小確定已編譯查詢的速度快慢。當使用上例所示的大型集合時,性能會極大下降。
4.2 鏈接到需要重新編譯的查詢
按照上述同一示例,如果有第二個查詢依賴需要重新編譯的查詢,則整個第二個查詢也將重新編譯。以下示例說明了這種情況:
int[] ids = new int[10000];
...
using (var context = new MyContext())
{
var firstQuery = from entity in context.MyEntities
where ids.Contains(entity.Id)
select entity;
var secondQuery = from entity in context.MyEntities
where firstQuery.Any(otherEntity => otherEntity.Id == entity.Id)
select entity;
string results = secondQuery.ToList();
...
}
這是個一般示例,但說明了鏈接到 firstQuery 如何導致 secondQuery 無法緩存。如果 firstQuery 不是需要重新編譯的查詢,則會緩存 secondQuery。
5 NoTracking 查詢
5.1 禁用更改跟蹤,以降低狀態管理開銷
如果在只讀情況中,想要避免將對象加載到 ObjectStateManager 中的開銷,則可發出“無跟蹤”查詢。可在查詢層面上禁用更改跟蹤。
注意,盡管如此,禁用更改跟蹤將有效關閉對象緩存。當查詢實體時,我們無法通過從 ObjectStateManager 中拉出先前具體化的查詢結果來跳過具體化。如果要在相同上下文中重復查詢同一實體,啟用更改跟蹤實際上可提高性能。
當使用 ObjectContext 進行查詢時,ObjectQuery 和 ObjectSet 實例將在 MergeOption 設置后記住它,並且在它們上編寫的查詢將繼承父查詢的有效 MergeOption。當使用 DbContext 時,可通過對 DbSet 調用 AsNoTracking() 修飾符禁用跟蹤。
5.1.1 在使用 DbContext 時禁用對查詢的更改跟蹤
通過在查詢中鏈接對 AsNoTracking() 方法的調用,可將查詢模式切換到 NoTracking。與 ObjectQuery 不同,DbContext API 中的 DbSet 和 DbQuery 類沒有針對 MergeOption 的可變屬性。
var productsForCategory = from p in context.Products.AsNoTracking()
where p.Category.CategoryName == selectedCategory
select p;
5.1.2 使用 ObjectContext 在查詢層面上禁用更改跟蹤
var productsForCategory = from p in context.Products
where p.Category.CategoryName == selectedCategory
select p;
((ObjectQuery)productsForCategory).MergeOption = MergeOption.NoTracking;
5.1.3 使用 ObjectContext 禁用對整個實體集的更改跟蹤
context.Products.MergeOption = MergeOption.NoTracking;
var productsForCategory = from p in context.Products
where p.Category.CategoryName == selectedCategory
select p;
5.2 演示 NoTracking 查詢的性能優勢的測試指標
在這個測試中,通過比較針對 Navision 模型的跟蹤和無跟蹤查詢,我們探討填充 ObjectStateManager 的成本。有關 Navision 模型的說明以及執行的查詢類型,請參見附錄。在這個測試中,我們循環訪問查詢列表,對每個查詢執行一次。我們進行了兩種不同的測試,一次使用 NoTracking 查詢,一次使用“AppendOnly”的默認合並選項。每種測試都進行 3 遍,取測試結果的平均值。在這些測試之間,我們清除了 SQL Server 上的查詢緩存,並且通過運行以下命令縮小了 tempdb:
- DBCC DROPCLEANBUFFERS
- DBCC FREEPROCCACHE
- DBCC SHRINKDATABASE (tempdb, 0)
測試結果:
測試類型 | 平均結果(3 次) |
NoTracking 查詢 | 所用秒數=315.63,工作集=588997973 |
AppendOnly 查詢 | 所用秒數=335.43,工作集=629760000 |
在這些測試中,填充 ObjectStateManager 所用時間多出 6%,所占內存多出 6%。
6 查詢執行選項
實體框架提供了幾種不同查詢方式。下面介紹以下選項,比較每個選項的優缺點,研究它們的性能特點:
- LINQ to Entities。
- 無跟蹤 LINQ to Entities。
- ObjectQuery 上的實體 SQL。
- EntityCommand 上的實體 SQL。
- ExecuteStoreQuery。
- SqlQuery。
- CompiledQuery。
6.1 LINQ to Entities 查詢
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
優點
- 適用於 CUD 操作。
- 完全具體化的對象。
- 以編程語言內置語法編寫最為簡單。
- 良好的性能。
Cons
- 某些技術限制,例如:
- 將 DefaultIfEmpty 用於 OUTER JOIN 查詢的模式導致查詢比實體 SQL 中的簡單 OUTER JOIN 語句更加復雜。
- 在一般模式匹配中仍無法使用 LIKE。
6.2 無跟蹤 LINQ to Entities 查詢
context.Products.MergeOption = MergeOption.NoTracking;
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
優點
- 性能比常規 LINQ 查詢更高。
- 完全具體化的對象。
- 以編程語言內置語法編寫最為簡單。
Cons
- 不適用於 CUD 操作。
- 某些技術限制,例如:
- 將 DefaultIfEmpty 用於 OUTER JOIN 查詢的模式導致查詢比實體 SQL 中的簡單 OUTER JOIN 語句更加復雜。
- 在一般模式匹配中仍無法使用 LIKE。
6.3 ObjectQuery 上的實體 SQL
ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");
優點
- 適用於 CUD 操作。
- 完全具體化的對象。
- 支持查詢計划緩存。
Cons
- 涉及文本查詢字符串,與內置在語言中的查詢構造相比,這些字符串更容易出現用戶錯誤。
6.4 Entity Command 上的實體 SQL
EntityCommand cmd = eConn.CreateCommand();
cmd.CommandText = "Select p From NorthwindEntities.Products As p Where p.Category.CategoryName = 'Beverages'";
using (EntityDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
while (reader.Read())
{
// 手動“具體化”產品
}
}
優點
- 支持查詢計划緩存。
Cons
- 涉及文本查詢字符串,與內置在語言中的查詢構造相比,這些字符串更容易出現用戶錯誤。
- 不適用於 CUD 操作。
- 結果不自動進行具體化,並且必須從數據讀取器中讀取。
6.5 SqlQuery 和 ExecuteStoreQuery
數據庫上的 SqlQuery:
// 使用它獲得實體,而不是跟蹤實體
var q1 = context.Database.SqlQuery<Product>("select * from products");
DbSet 上的 SqlQuery:
// 使用它來獲得實體,而不是跟蹤實體
var q2 = context.Products.SqlQuery("select * from products");
ExecyteStoreQuery:
ObjectResult<Product> beverages = context.ExecuteStoreQuery<Product>(
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued, P.DiscontinuedDate
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
優點
- 通常性能最快,因為繞開了計划編譯器。
- 完全具體化的對象。
- 在從 DbSet 使用時適用於 CUD 操作。
Cons
- 查詢為文本,容易出錯。
- 通過使用存儲語義,而不是概念語義,將查詢綁定到了特定后端。
- 當存在繼承時,手工查詢需要說明所請求類型的映射條件。
6.6 CompiledQuery
private static readonly Func<NorthwindEntities, string, IQueryable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
(NorthwindEntities context, string categoryName) =>
context.Products.Where(p => p.Category.CategoryName == categoryName)
);
…
var q = context.InvokeProductsForCategoryCQ("Beverages");
優點
- 性能最多比常規 LINQ 查詢高 7%。
- 完全具體化的對象。
- 適用於 CUD 操作。
Cons
- 更高的復雜性和編程開銷。
- 如果編寫在已編譯查詢頂部,不會提高性能。
- 某些 LINQ 查詢無法編寫為 CompiledQuery,例如,匿名類型的投影。
6.7 不同查詢選項的性能比較
為比較不同查詢選項的性能,我們創建了 5 個單獨的測試類型,我們使用不同的查詢選項來選擇類別名稱為“Beverages”的所有產品。每個迭代均包含創建上下文的成本,以及對所有返回的實體進行具體化的成本。先不計時運行 10 次迭代,然后計算 1000 次計時迭代的總和。所示結果是從每個測試的 5 次運行中獲得的中值運行。有關更多信息,請參見附錄 B,其中包含該測試的代碼。
注意:為求完整,我們包含了在 EntityCommand 上執行實體 SQL 查詢的測試類型。但由於沒有為這些查詢具體化結果,因此不必進行同類比較。該測試包含非常接近的具體化近似值,以便盡量做出更公平的比較。
在測試中還使用了簡單的微基准,沒有對上下文創建進行計時。我們在受控環境中測量了對一組非緩存實體進行的 5000 次查詢。這些數字將加以采用,同時警告:它們不反映應用程序生成的實際數字,但卻是非常准確的測量值,它們體現了在對不同查詢選項進行同類比較時存在多少性能差異。考慮到實際情況,足夠接近的數字可視為相等,始終以毫秒為單位:
7 設計時間性能注意事項
7.1 繼承策略
在使用實體框架時的另一個性能注意事項是所使用的繼承策略。實體框架支持 3 個基本類型的繼承及其組合:
- 每個層次結構一張表 (TPH) — 每個繼承集均映射到具有鑒別器列的表,以指示行中要表示層次結構中的特定類型。
- 每個類型一張表 (TPT) — 每個類型在數據庫中都有自己的表;子表僅定義父表未包含的列。
- 每個類一張表 (TPC) — 每個類型在數據庫中都有自己的完整表;子表定義它們的所有字段,包括父類型中定義的字段。
如果模型使用 TPT 繼承,則生成的查詢將比使用其他繼承策略生成的查詢更加復雜,這可能導致對存儲的執行時間更長。在 TPT 模型上生成查詢並具體化最終對象一般需要更長的時間。
請參見“在實體框架中使用 TPT(每個類型一張表)繼承時的性能注意事項”MSDN 博客文章:http://blogs.msdn.com/b/adonet/archive/2010/08/17/performance-considerations-when-using-tpt-table-per-type-inheritance-in-the-entity-framework.aspx。
7.1.1 在 Model First 或 Code First 應用程序中避免使用 TPT
當在具有 TPT 架構的現有數據庫上創建模型時,沒有許多選擇。但當使用 Model First 或 Code First 創建應用程序時,由於性能問題,應避免使用 TPT 繼承。
當在 Entity Designer 向導中使用 Model First 時,將獲得針對模型中任何繼承的 TPT。如果要轉換到采用 Model First 的 TPH 繼承策略,可使用 Visual Studio 庫提供的“Entity Designer Database Generation Power Pack”( http://visualstudiogallery.msdn.microsoft.com/df3541c3-d833-4b65-b942-989e7ec74c87/ )。
當使用 Code First 配置具有繼承的模型的映射時,在默認情況下實體框架將使用 TPH,即,繼承層次結構中的所有實體都映射到同一表。有關更多詳細信息,請參見 MSDN 雜志文章“ADO.NET 實體框架 4.1 中的 Code First”的“使用 Fluent API 進行映射”一節 (http://msdn.microsoft.com/magazine/hh126815.aspx )。
7.2 升級到 EF 5 以縮短模型生成時間
EF 5 中實現了對生成模型存儲層 (SSDL) 的算法的 SQL Server 特定改進,在安裝 Dev10 SP1 時,此改進作為對 EF 4 的更新。以下測試結果演示在生成超大模型(在本例中是 Navision 模型)時的改進。有關更多詳細信息,請參見附錄 C。
配置 | 持續時間 | 模型生成各階段的百分比細分 |
Visual Studio 2010。 具有 1005 個實體集和 4227 個關聯集的模型。 |
所用秒數=16835.08 (4:40:35) | SSDL 生成:2 小時 27 分鍾 映射生成:<1 分鍾 CSDL 生成:<1 分鍾 ObjectLayer 生成:<1 分鍾 視圖生成:2 小時 14 分鍾 |
Visual Studio 2010 SP1。 具有 1005 個實體集和 4227 個關聯集的模型。 |
所用秒數=6813.18 (1:53:33) | SSDL 生成:<1 分鍾 映射生成:<1 分鍾 CSDL 生成:<1 分鍾 ObjectLayer 生成:<1 分鍾 視圖生成:1 小時 53 分鍾 |
值得注意的是,當生成 SSDL 時,加載時間幾乎完全用在 SQL Server 上,而客戶端開發計算機正在空閑地等待從服務器返回結果。這一改進對 DBA 尤其有用。還值得注意的是,實質上模型生成的全部成本現在是在視圖生成中產生的。
7.3 利用 Database First 和 Model First 拆分大模型
隨着模型大小的增加,設計器圖面變得雜亂且難以使用。一般認為具有超過 300 個實體的模型太大,難以有效使用設計器。我們的一位開發組長 Srikanth Mandadi 寫了以下博客文章,介紹拆分大模型的幾種選擇: http://blogs.msdn.com/b/adonet/archive/2008/11/25/working-with-large-models-in-entity-framework-part-2.aspx。
這篇文章是為實體框架的第一個版本所寫的,但這些步驟仍適用。
7.4 使用 Entity Data Source 控件時的性能注意事項
我們已看到了多線程性能和壓力測試中的用例情況,在這些情況下,使用 EntityDataSource 控件的 Web 應用程序的性能大幅下降。其根本原因是 EntityDataSource 反復調用 Web 應用程序所引用的程序集上的 MetadataWorkspace.LoadFromAssembly,以便發現將用作實體的類型。
解決方案是將 EntityDataSource 的 ContextTypeName 設置為派生 ObjectContext 類的類型名稱。這會關閉掃描所有引用的程序集以查找是否有實體類型的機制。
設置 ContextTypeName 字段還會防止以下功能問題:當 .NET 4.0 中的 EntityDataSource 無法通過反射從程序集中加載類型時,它會引發 ReflectionTypeLoadException。該問題在 .NET 4.5 中已得到修復。
7.5 POCO 實體與更改跟蹤代理
通過實體框架可將自定義數據類與數據模型一同使用,無需對數據類本身進行任何修改。這意味着可以將“純舊式”CLR 對象 (POCO)(例如,現有的域對象)與數據模型一起使用。這些 POCO 數據類(也稱為缺少持續性的對象,映射到在數據模型中定義的實體)支持與實體數據模型工具生成的實體類型相同的大部分查詢、插入、更新和刪除行為。
實體框架還能夠創建從 POCO 類型派生的代理類,如果需要對 POCO 實體啟用延遲加載和自動更改跟蹤等功能,可以使用這些類。POCO 類必須符合某些要求才可使實體框架使用代理,如這篇文章所述: http://msdn.microsoft.com/library/dd468057.aspx。
每次實體的任何屬性值更改時,更改跟蹤代理都將通知對象狀態管理器,因此實體框架始終知道這些實體的實際狀態。這是通過以下方式實現的:將通知事件添加到屬性 setter 方法的主體中,讓對象狀態管理器處理這些事件。注意,實體框架創建了更多事件集,因此創建代理實體一般比創建非代理 POCO 實體成本更高。
當 POCO 實體沒有更改跟蹤代理時,通過將實體的內容與先前保存的狀態的副本進行比較來查找更改。如果上下文中有許多實體,或者實體有大量屬性,這種深入比較將變成一個冗長的過程,即使自上次比較以來它們均沒有更改也是如此。
總結:在創建更改跟蹤代理時性能會下降,但如果實體有許多屬性,或者模型中有許多實體,更改跟蹤將有助於加快更改檢測過程。對於實體數量沒有過多增長、有少量屬性的實體,更改跟蹤代理的優勢可能不明顯。
8 加載相關實體
8.1 延遲加載與預先加載
實體框架提供了多種不同方式來加載與目標實體相關的實體。例如,當查詢產品時,可通過不同方式將相關訂單加載到對象狀態管理器中。從性能觀點來講,在加載相關實體時要考慮的最大問題將是使用延遲加載還是預先加載。
當使用預先加載時,相關實體連同目標實體集一同加載。在查詢中使用 Include 語句來指示要獲取哪些相關實體。
當使用延遲加載時,初始查詢僅獲取目標實體集。但只要訪問導航屬性,便會發出對存儲的另一個查詢,以加載相關實體。
已加載了實體后,對該實體的任何進一步查詢都會從對象狀態管理器直接加載它,無論正在使用延遲加載還是預先加載。
8.2 如果在延遲加載和預先加載之間做出選擇
重要的是了解延遲加載與預先加載之間的區別,這樣才能做出適合您應用程序的正確選擇。這將有助於您對照數據庫評估多個請求之間的權衡,而不是評估可能包含較大負載的單個請求。在應用程序的某些部分中使用預先加載而在其他部分中使用延遲加載可能是適當的做法。
舉一個有關在后台發生的情況的例子,假設您想要查詢住在英國的客戶以及他們的訂單數。
使用預先加載
using (NorthwindEntities context = new NorthwindEntities())
{
var ukCustomers = context.Customers.Include(c => c.Orders).Where(c => c.Address.Country == "UK");
var chosenCustomer = AskUserToPickCustomer(ukCustomers);
Console.WriteLine("Customer Id: {0} has {1} orders", customer.CustomerID, customer.Orders.Count);
}
使用延遲加載
using (NorthwindEntities context = new NorthwindEntities())
{
context.ContextOptions.LazyLoadingEnabled = true;
//注意在該查詢中,Include 方法調用正在丟失
var ukCustomers = context.Customers.Where(c => c.Address.Country == "UK");
var chosenCustomer = AskUserToPickCustomer(ukCustomers);
Console.WriteLine("Customer Id: {0} has {1} orders", customer.CustomerID, customer.Orders.Count);
}
當使用預先加載時,將發出一個返回所有客戶和所有訂單的查詢。存儲命令看起來像:
[Project1].[C1] AS [C1],
[Project1].[CustomerID] AS [CustomerID],
[Project1].[CompanyName] AS [CompanyName],
[Project1].[ContactName] AS [ContactName],
[Project1].[ContactTitle] AS [ContactTitle],
[Project1].[Address] AS [Address],
[Project1].[City] AS [City],
[Project1].[Region] AS [Region],
[Project1].[PostalCode] AS [PostalCode],
[Project1].[Country] AS [Country],
[Project1].[Phone] AS [Phone],
[Project1].[Fax] AS [Fax],
[Project1].[C2] AS [C2],
[Project1].[OrderID] AS [OrderID],
[Project1].[CustomerID1] AS [CustomerID1],
[Project1].[EmployeeID] AS [EmployeeID],
[Project1].[OrderDate] AS [OrderDate],
[Project1].[RequiredDate] AS [RequiredDate],
[Project1].[ShippedDate] AS [ShippedDate],
[Project1].[ShipVia] AS [ShipVia],
[Project1].[Freight] AS [Freight],
[Project1].[ShipName] AS [ShipName],
[Project1].[ShipAddress] AS [ShipAddress],
[Project1].[ShipCity] AS [ShipCity],
[Project1].[ShipRegion] AS [ShipRegion],
[Project1].[ShipPostalCode] AS [ShipPostalCode],
[Project1].[ShipCountry] AS [ShipCountry]
FROM ( SELECT
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[CompanyName] AS [CompanyName],
[Extent1].[ContactName] AS [ContactName],
[Extent1].[ContactTitle] AS [ContactTitle],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
[Extent1].[Region] AS [Region],
[Extent1].[PostalCode] AS [PostalCode],
[Extent1].[Country] AS [Country],
[Extent1].[Phone] AS [Phone],
[Extent1].[Fax] AS [Fax],
1 AS [C1],
[Extent2].[OrderID] AS [OrderID],
[Extent2].[CustomerID] AS [CustomerID1],
[Extent2].[EmployeeID] AS [EmployeeID],
[Extent2].[OrderDate] AS [OrderDate],
[Extent2].[RequiredDate] AS [RequiredDate],
[Extent2].[ShippedDate] AS [ShippedDate],
[Extent2].[ShipVia] AS [ShipVia],
[Extent2].[Freight] AS [Freight],
[Extent2].[ShipName] AS [ShipName],
[Extent2].[ShipAddress] AS [ShipAddress],
[Extent2].[ShipCity] AS [ShipCity],
[Extent2].[ShipRegion] AS [ShipRegion],
[Extent2].[ShipPostalCode] AS [ShipPostalCode],
[Extent2].[ShipCountry] AS [ShipCountry],
CASE WHEN ([Extent2].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
FROM [dbo].[Customers] AS [Extent1]
LEFT OUTER JOIN [dbo].[Orders] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
WHERE N'UK' = [Extent1].[Country]
) AS [Project1]
ORDER BY [Project1].[CustomerID] ASC, [Project1].[C2] ASC
當使用延遲加載時,最初將發出以下查詢:
SELECT
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[CompanyName] AS [CompanyName],
[Extent1].[ContactName] AS [ContactName],
[Extent1].[ContactTitle] AS [ContactTitle],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
[Extent1].[Region] AS [Region],
[Extent1].[PostalCode] AS [PostalCode],
[Extent1].[Country] AS [Country],
[Extent1].[Phone] AS [Phone],
[Extent1].[Fax] AS [Fax]
FROM [dbo].[Customers] AS [Extent1]
WHERE N'UK' = [Extent1].[Country]
每次訪問客戶的訂單導航屬性時,便會對存儲發出另一個查詢,如下:
exec sp_executesql N'SELECT
[Extent1].[OrderID] AS [OrderID],
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[EmployeeID] AS [EmployeeID],
[Extent1].[OrderDate] AS [OrderDate],
[Extent1].[RequiredDate] AS [RequiredDate],
[Extent1].[ShippedDate] AS [ShippedDate],
[Extent1].[ShipVia] AS [ShipVia],
[Extent1].[Freight] AS [Freight],
[Extent1].[ShipName] AS [ShipName],
[Extent1].[ShipAddress] AS [ShipAddress],
[Extent1].[ShipCity] AS [ShipCity],
[Extent1].[ShipRegion] AS [ShipRegion],
[Extent1].[ShipPostalCode] AS [ShipPostalCode],
[Extent1].[ShipCountry] AS [ShipCountry]
FROM [dbo].[Orders] AS [Extent1]
WHERE [Extent1].[CustomerID] = @EntityKeyValue1',N'@EntityKeyValue1 nchar(5)',@EntityKeyValue1=N'AROUT'
有關更多信息,請參見“加載相關對象”MSDN 文章: http://msdn.microsoft.com/library/bb896272.aspx。
8.2.1 延遲加載與預先加載備忘單
選擇預先加載與延遲加載沒有一刀切的方法。首先盡量了解這兩個策略之間的區別,這樣才能做出明智決定;此外考慮代碼是否適合下列任何一種情況:
情況 | 建議 | |
是否需要從提取的實體中訪問許多導航屬性? | 否 | 這兩個選擇可能都行。但如果查詢所產生的負載不太大,則通過使用預先加載可能會提高性能,因為這需要較少的網絡往返即可具體化對象。 |
是 | 如果需要從實體訪問許多導航屬性,最好在采用預先加載的查詢中使用多個 include 語句。包含的實體越多,查詢將返回的負載就越大。如果查詢包含三個或更多實體,考慮轉為延遲加載。 | |
是否確切知道在運行時將需要什么數據? | 否 | 更適合采用延遲加載。否則,可能結果是查詢不需要的數據。 |
是 | 可能最適合采用預先加載;這有助於更快速地加載整個集。如果查詢需要提取大量數據,而速度非常慢,則嘗試延遲加載。 | |
代碼是否要在遠離數據庫的位置執行?(更長的網絡延遲) | 否 | 當網絡延遲不是問題時,使用延遲加載可能會簡化代碼。請注意,應用程序的拓撲結構可能改變,因此不要認為數據庫鄰近是理所當然的。 |
是 | 當網絡是要考慮的問題時,只有您才能確定適合采用哪種方式。一般預先加載會更好,因為需要的往返次數更少。 |
8.2.2 在使用多個 Include 時的性能問題
當我們聽說涉及服務器響應時間問題的性能問題時,根源通常是使用多個 Include 語句的查詢。盡管在查詢中包含相關實體可實現強大功能,還是應該了解實際情況。
包含多個 Include 語句的查詢需要相對較長的時間通過內部計划編譯器,才能生成存儲命令。這些時間中的大部分用於嘗試優化最終查詢。根據映射,生成的存儲命令將包含針對每個 Include 的 Outer Join 或 Union。這類查詢將在單個負載中從數據庫獲取大量已連接的圖形,從而惡化所有帶寬問題,在負載存在大量冗余時(即,具有能夠在一到多方向中遍歷關聯的多個 Include 級別),更是如此。
通過以下方式可以查看查詢是否存在要返回超大量負載的情況:使用 ToTraceString 訪問針對查詢的基本 TSQL,在 SQL Server Management Studio 中執行存儲命令,查看負載的大小。在這種情況下,可嘗試減少查詢中的 Include 語句數,僅獲取所需的數據。也可將查詢分成多個更小的子查詢序列,例如:
在拆分查詢前:
using (NorthwindEntities context = new NorthwindEntities())
{
var customers = from c in context.Customers.Include(c => c.Orders)
where c.LastName.StartsWith(lastNameParameter)
select c;
foreach (Customer customer in customers)
{
...
}
}
在拆分查詢后:
using (NorthwindEntities context = new NorthwindEntities())
{
var orders = from o in context.Orders
where o.Customer.LastName.StartsWith(lastNameParameter)
select o;
orders.Load();
var customers = from c in context.Customers
where c.LastName.StartsWith(lastNameParameter)
select c;
foreach (Customer customer in customers)
{
...
}
}
這僅對跟蹤查詢有效,因為我們要利用上下文的功能自動執行標識解析和關聯修復。
至於延遲加載,權衡結果將是用更多查詢實現較小負載。還可使用各屬性的投影顯式地從每個實體中僅選擇所需的數據,但是,在這種情況下不會加載實體,也不支持更新。
8.2.3 屬性的延遲加載
實體框架當前不支持標量或復雜屬性的延遲加載。但是,如果表中包含 BLOB 等大型對象,可以使用表拆分功能將大屬性分成單獨實體。例如,假設有一個 Product 表,它包含一個變長二進制圖片列。如果不需要經常訪問查詢中的這個屬性,則可使用表拆分功能僅獲取通常需要的實體部分。如果明確需要表示產品圖片的實體時,僅加載該實體。
要說明如何啟用表拆分功能,一個很好的資源是 Gil Fink 的博客文章“實體框架中的表拆分”:http://blogs.microsoft.co.il/blogs/gilf/archive/2009/10/13/table-splitting-in-entity-framework.aspx。
9 調查性能
9.1 使用 Visual Studio 探查器
如果遇到實體框架的性能問題,可使用探查器(如 Visual Studio 內置探查器)查看應用程序將時間用在了哪里。我們使用這個工具生成了博客文章“探究 ADO.NET 實體框架的性能 - 第 1 部分”( http://blogs.msdn.com/b/adonet/archive/2008/02/04/exploring-the-performance-of-the-ado-net-entity-framework-part-1.aspx) 中的餅圖,說明在冷查詢和熱查詢過程中實體框架將時間用在了哪里。
數據與建模客戶顧問團隊的博客文章“使用 Visual Studio 2010 探查器分析實體框架”提供了一個真實示例,說明他們如何使用這個探查器調查性能問題。http://blogs.msdn.com/b/dmcat/archive/2010/04/30/profiling-entity-framework-using-the-visual-studio-2010-profiler.aspx。這篇文章是針對 Windows 應用程序編寫的。如果需要分析 Web 應用程序,則 VSPerfCmd 工具可能比使用 Visual Studio 更有效。
9.2 應用程序/數據庫分析
通過工具(如 Visual Studio 內置探查器)可以發現應用程序將時間用在哪里。另外還有一種探查器,可根據需要在生產或預生產中動態分析正在運行的應用程序,以及查找數據庫訪問的常見缺陷和反模式。
實體框架 Profiler ( http://efprof.com) 和 ORMProfiler (http://ormprofiler.com) 是兩個商用探查器。
如果應用程序是使用 Code First 的 MVC 應用程序,則可使用 StackExchange 的 MiniProfiler。Scott Hanselman 在他的博客中介紹了這個工具:http://www.hanselman.com/blog/NuGetPackageOfTheWeek9ASPNETMiniProfilerFromStackExchangeRocksYourWorld.aspx。
有關分析應用程序數據庫活動的更多信息,請參見 Julie Lerman 的 MSDN 雜志文章,標題為“分析實體框架中的數據庫活動”:http://msdn.microsoft.com/magazine/gg490349.aspx。
10 附錄
10.1 A. 測試環境
10.1.1 環境 1
這個環境使用了 2 計算機設置,數據庫與客戶端應用程序不在同一計算機上,而是在單獨的計算機上。計算機在同一機架中,因此網絡延遲相對較低,但比單機環境更接近實際。
10.1.1.1 應用程序服務器
10.1.1.1.1 軟件環境
- 操作系統名稱:Windows Server 2008 R2 Enterprise SP1。
- Visual Studio 2010 — 旗艦版。
- Visual Studio 2010 SP1(僅用於某些比較)。
10.1.1.1.2 硬件環境
- 雙處理器:Intel(R) Xeon(R) CPU L5520,2.27GHz,2261 Mhz,4 核 8 邏輯處理器。
- 24 GB Ram。
- 分成 4 個分區的 136 GB SCSI 驅動器。
10.1.1.2 DB 服務器
10.1.1.2.1 軟件環境
- 操作系統名稱:Windows Server 2008 R2 Enterprise SP1。
- SQL Server 2008 R2。
10.1.1.2.2 硬件環境
- 單處理器:Intel(R) Xeon(R) CPU L5520,2.27GHz,2261 Mhz,4 核 8 邏輯處理器。
- 8 GB Ram。
- 分成 4 個分區的 465 GB ATA 驅動器。
10.1.1.3 在這個環境中收集的測試指標
- 視圖生成。
- 查詢計划緩存。
- 禁用更改跟蹤。
- 升級到 Dev10 SP1 和 Dev11 以縮短模型生成時間。
10.1.2 環境 2
這個環境使用單工作站。客戶端應用程序和數據庫在同一計算機上。
10.1.2.1 軟件環境
- 操作系統名稱:Windows Server 2008 R2 Enterprise SP1。
- SQL Server 2008 R2。
10.1.2.2 硬件環境
- 單處理器:Intel(R) Xeon(R) CPU L5520,2.27GHz,2261 Mhz,4 核 8 邏輯處理器。
- 8 GB Ram。
- 分成 4 個分區的 465 GB ATA 驅動器。
10.1.2.3 在這個環境中收集的測試指標
- 查詢執行比較。
10.2 B. 查詢性能比較測試
using System.Data;
using System.Data.Common;
usingSystem.Data.Entity.Infrastructure;
using System.Data.EntityClient;
using System.Data.Objects;
using System.Linq;
using NavisionDbContext;
using NavisionObjectContext;
using PerfBVTHarness;
namespace NavisionObjectContext
{
public partial class NorthwindEntities : ObjectContext
{
private static readonly Func<NorthwindEntities, string, IQueryable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
(NorthwindEntities context, string categoryName) =>
context.Products.Where(p => p.Category.CategoryName == categoryName)
);
public IQueryable<Product> InvokeProductsForCategoryCQ(string categoryName)
{
return productsForCategoryCQ(this, categoryName);
}
}
}
namespace QueryComparison
{
public class QueryTypePerfComparison
{
private static string entityConnectionStr = @"metadata=res://*/NorthwindModel.csdl|res://*/NorthwindModel.ssdl|res://*/NorthwindModel.msl;provider=System.Data.SqlClient;provider connection string='data source=.\sqlexpress;initial catalog=NorthwindEF;integrated security=True;multipleactiveresultsets=True;App=EntityFramework'";
[Test("LinqQueryObjectContext",
Description = "Query for beverages and materialize results",
WarmupIterations = 10,
TestIterations = 1000)]
public void LINQIncludingContextCreation()
{
using (NorthwindEntities context = new NorthwindEntities())
{
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
}
[Test("LinqQueryNoTrackingObjectContext",
Description = "Query for beverages and materialize results - NoTracking",
WarmupIterations = 10,
TestIterations = 1000)]
public void LINQNoTracking()
{
using (NorthwindEntities context = new NorthwindEntities())
{
context.Products.MergeOption = MergeOption.NoTracking;
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
}
[Test("CompiledQuery",
Description = "Query for beverages and materialize results using a CompiledQuery",
WarmupIterations = 10,
TestIterations = 1000)]
public void CompiledQuery()
{
using (NorthwindEntities context = new NorthwindEntities())
{
var q = context.InvokeProductsForCategoryCQ("Beverages");
q.ToList();
}
}
[Test("ObjectQuery",
Description = "Query for beverages and materialize results using an ObjectQuery",
WarmupIterations = 10,
TestIterations = 1000)]
public void ObjectQuery()
{
using (NorthwindEntities context = new NorthwindEntities())
{
ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");
products.ToList();
}
}
[Test("EntityCommand",
Description = "Query for beverages on an EntityCommand and materialize results by reading from a DataReader",
WarmupIterations = 10,
TestIterations = 1000)]
public void EntityCommand()
{
using (EntityConnection eConn = new EntityConnection(entityConnectionStr))
{
eConn.Open();
EntityCommand cmd = eConn.CreateCommand();
cmd.CommandText = "Select p From NorthwindEntities.Products As p Where p.Category.CategoryName = 'Beverages'";
using (EntityDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
List<Product> productsList = new List<Product>();
while (reader.Read())
{
DbDataRecord record = (DbDataRecord)reader.GetValue(0);
// 通過訪問每個字段和值對產品進行“具體化”。因為要具體化產品,我們沒有任何嵌套數據讀取器或記錄。
int fieldCount = record.FieldCount;
// 將所有產品均視為 Product,即使它們是子類型 DiscontinuedProduct 也是如此。
Product product = new Product();
product.ProductID = record.GetInt32(0);
product.ProductName = record.GetString(1);
product.QuantityPerUnit = record.GetString(2);
product.UnitPrice = record.GetDecimal(3);
product.UnitsInStock = record.GetInt16(4);
product.UnitsOnOrder = record.GetInt16(5);
product.ReorderLevel = record.GetInt16(6);
productsList.Add(product);
}
}
}
}
[Test("ExecuteStoreQuery",
Description = "Query for beverages using ExecuteStoreQuery",
WarmupIterations = 10,
TestIterations = 1000)]
public void ExecuteStoreQuery()
{
using (NorthwindEntities context = new NorthwindEntities())
{
ObjectResult<Product> beverages = context.ExecuteStoreQuery<Product>(
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
beverages.ToList();
}
}
[Test("SqlQueryOnDatabase",
Description = "Query for beverages using SqlQuery on Database",
WarmupIterations = 10,
TestIterations = 1000)]
public void ExecuteStoreQuery()
{
using (DbContextNorthwindEntities context = new DbContextNorthwindEntities())
{
IEnumerable<NavisionDbContext.Product> beverages = context.Database.SqlQuery<Product>(
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
beverages.ToList();
}
}
[Test("SqlQueryOnDbSet",
Description = "Query for beverages using SqlQuery on Database",
WarmupIterations = 10,
TestIterations = 1000)]
public void ExecuteStoreQuery()
{
using (DbContextNorthwindEntities context = new DbContextNorthwindEntities())
{
DbSqlQuery<NavisionDbContext.Product> beverages = context.Products.SqlQuery (
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
beverages.ToList();
}
}
[Test("LinqQueryDbContext",
Description = "Query for beverages and materialize results",
WarmupIterations = 10,
TestIterations = 1000)]
public void LINQIncludingContextCreationDbContext()
{
using (DbContextNorthwindEntities context = new DbContextNorthwindEntities())
{
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
}
[Test("LinqQueryNoTrackingDbContext",
Description = "Query for beverages and materialize results - NoTracking",
WarmupIterations = 10,
TestIterations = 1000)]
public void LINQNoTrackingDbContext ()
{
using (DbContextNorthwindEntities context = new DbContextNorthwindEntities())
{
var q = context.Products.AsNoTracking().Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
}
}
}
10.3 C. Navision 模型
Navision 數據庫是一個用於演示 Microsoft Dynamics – NAV 的大型數據庫。生成的概念模型包含 1005 個實體集和 4227 個關聯集。在測試中使用的模型是“扁平的”— 尚未添加繼承。
10.3.1 用於 Navision 測試的查詢
在 Navision 模型中使用的查詢列表包含 3 個類別的實體 SQL 查詢:
10.3.1.1 查找
無聚合的簡單查找查詢
- 計數:16232
- 示例:
<Query complexity="Lookup">
<CommandText>Select value distinct top(4) e.Idle_Time From NavisionFKContext.Session as e</CommandText>
</Query>
10.3.1.2 SingleAggregating
具有多個聚合但沒有小計的正常 BI 查詢(單一查詢)
- 計數:2313
- 示例:
<Query complexity="SingleAggregating">
<CommandText>NavisionFK.MDF_SessionLogin_Time_Max()</CommandText>
</Query>
其中 MDF_SessionLogin_Time_Max() 在模型中定義為:
<Function Name="MDF_SessionLogin_Time_Max" ReturnType="Collection(DateTime)">
<DefiningExpression>SELECT VALUE Edm.Min(E.Login_Time) FROM NavisionFKContext.Session as E</DefiningExpression>
</Function>
10.3.1.3 AggregatingSubtotals
具有聚合和小計的 BI 查詢(通過 union all)
- 計數:178
- 示例:
<CommandText>
using NavisionFK;
function AmountConsumed(entities Collection([CRONUS_International_Ltd__Zone])) as
(
Edm.Sum(select value N.Block_Movement FROM entities as E, E.CRONUS_International_Ltd__Bin as N)
)
function AmountConsumed(P1 Edm.Int32) as
(
AmountConsumed(select value e from NavisionFKContext.CRONUS_International_Ltd__Zone as e where e.Zone_Ranking = P1)
)
----------------------------------------------------------------------------------------------------------------------
(
select top(10) Zone_Ranking, Cross_Dock_Bin_Zone, AmountConsumed(GroupPartition(E))
from NavisionFKContext.CRONUS_International_Ltd__Zone as E
where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed
group by E.Zone_Ranking, E.Cross_Dock_Bin_Zone
)
union all
(
select top(10) Zone_Ranking, Cast(null as Edm.Byte) as P2, AmountConsumed(GroupPartition(E))
from NavisionFKContext.CRONUS_International_Ltd__Zone as E
where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed
group by E.Zone_Ranking
)
union all
{
Row(Cast(null as Edm.Int32) as P1, Cast(null as Edm.Byte) as P2, AmountConsumed(select value E
from NavisionFKContext.CRONUS_International_Ltd__Zone as E
where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed))
}</CommandText>
<參數>
<Parameter Name="MinAmountConsumed" DbType="Int32" Value="10000" />
</Parameters>
</Query>