程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 數據點-預編譯LINQ查詢

數據點-預編譯LINQ查詢

編輯:關於.NET

在應用程序中使用 LINQ to SQL 或 LINQ to Entities 時,有必要考慮對您 創建並重復執行的任何查詢進行預編譯。我經常在埋頭苦干一項特定任務時忘了 利用預編譯查詢,等我意識到時為時已晚。這很像“異常處理病”,即開發人員 試圖在事發後將異常處理強行加入應用程序中。

然而,即使您已經實施了此項重要的性能增強方法,往往也只是徒勞。您可 能會發現預期的性能增強並未實現,但原因(和解決方法)可能仍懸而未決。

在本篇專欄文章中,我首先將解釋如何預編譯查詢,然後將重點講述在 Web 應用程序、服務和其他方案中導致預編譯無用的原因。您將學習如何確保在回發 、短期服務操作以及其他會導致關鍵實例超出作用域的代碼中獲得性能優勢。

預編譯查詢

在龐大的查詢執行中,將 LINQ 查詢轉換為相關的存儲查詢(例如,數據庫 執行的 T-SQL)需要較高的成本。圖 1 顯示了將 LINQ to Entities 查詢轉換 為存儲查詢時涉及的流程。

圖 1 將 LINQ 查詢轉換為相關的存儲查詢

實體框架團隊的博客文章“探討 ADO.NET 實體框架的性能 – 第 1 部分” (blogs.msdn.com/adonet/archive/2008/02/04/exploring-the-performance- of-the-ado-net-entity-framework-part-1.aspx) 對該流程進行了細分,並提 供了每個步驟的對應時間。注意:此篇文章基於 Microsoft .NET Framework 3.5 SP1 版本的實體框架,新版本中各步驟的時間分配可能發生了變化。但查詢 執行流程中的預編譯成本仍然較高。

通過預編譯查詢,實體框架和 LINQ to SQL 可以重復使用存儲查詢並跳過每 次計算的冗余流程。例如,如果您的應用程序經常從數據存儲中檢索不同的客戶 ,您可能會有一個與下面類似的查詢:

Context.Customers.Where(c=>c.CustomerID==_custID)

在從一個執行進入下一個執行時,如果只有 _custID 參數發生了變化,為什 麼要浪費時間將此查詢反復轉置到 SQL 命令呢?

LINQ to SQL 和實體框架都啟用了查詢預編譯,但由於這兩個框架中的流程 不同,它們的 CompiledQuery 類也不同。LINQ to SQL 使用 System.Data.LINQ.CompiledQuery,而實體框架使用 System.Data.Objects.CompiledQuery。這兩種形式的 CompiledQuery 都允許您 傳入參數,而且都要求您傳入當前正在使用的 DataContext 或 ObjectContext 。從編碼的角度來說,它們在本質上是相同的。

CompiledQuery.Compile 方法以 Func 的形式返回一個委托,在需要時可以 調用該委托。

以下是實體框架的 CompiledQuery 類編譯的一個簡單查詢,由於是靜態查詢 ,因此不要求實例化:

C#

var _custByID = CompiledQuery.Compile<SalesEntities,  int, Customer>
   ((ctx, id) =>ctx.Customers.Where(c=> c.ContactID  == id).Single());

VB

Dim _custByID= CompiledQuery.Compile(Of SalesEntities,  Integer, Customer)
   (Function(ctx As ObjectContext, id As Integer)
   ctx.Customers.Where(Function(c) c.CustomerID =  custID).Single)

您可以在查詢表達式中使用 LINQ 方法或 LINQ 運算符。這些查詢是利用 LINQ 方法和 lambda 創建的。

與典型的泛型方法相比,該語法相對復雜,因此我將對該語法進行細分講述 。再次強調,Compile 方法的目標是創建一個可在以後調用的 Func(委托), 如下所示:

C#

CompiledQuery.Compile<SalesEntities, int, Customer>

VB

CompiledQuery.Compile(Of SalesEntities, Integer, Customer)

由於采用的是泛型方法,因此方法必須被告知傳入參數的類型以及調用委托 時的返回類型。至少,您必須傳入某種類型的 ObjectContext 或 DataContext (對於 LINQ to SQL)。您可以指定 System.Data.Objects.ObjectContext 或 從其派生的參數。在此例中,我明確地使用了與我的實體數據模型關聯的派生類 SalesEntities。

您也可以定義多個參數,但它們必須緊跟在上下文之後。在此例中,我指示 Compile,產生的預編譯查詢還應采用 int/整數參數。最後的類型描述了查詢的 返回類型,在此例中是 Customer 對象:

C#

((ctx, id) =>ctx.Customers.Where(c => c.ContactID ==  id).Single())

VB

Function(ctx As ObjectContext, id As Integer)
   ctx.Customers.Where(Function(c) c.CustomerID =  custID).Single

之前編譯方法的結果是下列委托:

C#

private System.Func<SalesEntities, int, Customer>  _custByID

Private _custByID As System.Func(Of SalesEntities, Integer,  Customer)

編譯查詢後,您今後只需在希望執行查詢時調用它,傳入 ObjectContext 或 DataContext 實例和所需任何其他參數。此處有一個名為 _commonContext 的實 例和一個名為 _custID 的變量:

Customer cust = _custByID.Invoke(_commonContext, _custID);

第一次調用委托時,此查詢將轉換為存儲查詢,並且將對此次轉換進行緩存 以供今後調用 Invoke 時重復使用。LINQ 可以跳過編譯查詢的任務,直接進入 執行。

確保預編譯查詢真正被使用

對於預編譯查詢,有一個不太明顯而且人們知之甚少的問題。很多開發人員 都想當然地認為查詢被緩存在應用程序進程中,並且一直保存在那裡。我原來也 這麼認為,因為除了一些不太引人注意的性能數字之外,沒有跡象表明不是這樣 。但是,當您實例化編譯查詢的對象超出作用域時,您也隨之失去了預編譯查詢 。您需要在每次使用時重新預編譯,因而完全喪失了預編譯帶來的好處。事實上 ,如果您只是執行 LINQ 查詢,由於 CLR 必須為委托進行額外的工作,您反而 要付出比正常情況更高的代價。

Rico Mariani 在他的博客文章“性能問題 #13 — Linq to SQL 編譯查詢成 本 — 解決方案”(blogs.msdn.com/ricom/archive/2008/01/14/performance- quiz-13-linq-to-sql-compiled-query-cost-solution.aspx) 中深入探討了使 用委托的成本。評論中的討論同樣具有啟發意義。

我看過一些博客文章,報道即使使用了預編譯查詢,LINQ to Entities 在 Web 應用程序中的性能仍令人無法接受。原因是每次回發頁面時,都要獲取新實 例化的上下文並重新預編譯查詢。預編譯查詢永遠得不到反復使用。只要您的上 下文生存期很短,您就會遇到相同的問題。在某些地方很容易發現這種問題,例 如在 Web 或 Windows Communication Foundation (WCF) 服務中;但在某些地 方不太容易發現這種問題,例如在未對其提供實例時將動態實例化新上下文的存 儲庫中。

您可以避免失去委托,方式是使用靜態變量(在 VB 中為共享變量)在進程 之間保留查詢,然後使用當前提供的任何上下文調用查詢。

在 Web 應用程序、WCF 服務和存儲庫中,ObjectContext 經常超出作用域, 我成功地利用下列模式使委托在整個應用程序進程中一直可用。您需要在將調用 查詢的類構造函數中聲明靜態委托。我在此處聲明的委托與之前創建的已編譯查 詢相符:

C#

static Func<ObjectContext, int, Customer> _custByID;

VB

Shared _custByID As Func(Of ObjectContext, Integer,  Customer)

有幾個可以編譯查詢地方。您可以在類構造函數中或在即將調用查詢之前編 譯查詢。此處的方法用於執行查詢並返回 Customer 對象:

public static Customer GetCustomer( int ID)
   {
    //test for an existing instance of the compiled  query
    if (_custByID == null)
    {
     _custByID = CompiledQuery.Compile<SalesEntities, int,  Customer>
      ((ctx, id) => ctx.Customers.Where(c =>  c.CustomerID == id).Single());
    }
    return _custByID.Invoke(_context, ID);
   }

此方法將使用已編譯查詢。首先,它將僅在需要時動態編譯查詢,我通過測 試了解是否已實例化查詢來確定是否需要編譯。如果您在類構造函數中編譯,也 需要執行相同的測試,以確信僅在必要時使用要編譯的資源。

由於委托 _custByID 是靜態的,因此即使其包含類超出作用域,該委托仍然 保持在內存中。因此,只要應用程序進程本身在作用域中,委托就存在;它不會 為 null,編譯步驟將被跳過。

預編譯查詢和投影

還有一些值得注意並且也容易發現的問題。第一個問題與投影有關,但它並 不針對不明智地重新編譯已預編譯查詢的問題。當您投影查詢中的列而非返回特 定類型時,您將始終獲得匿名類型。

定義查詢時不可能指定其返回類型,因為無法表示“匿名類型的類型”。如 果您希望將查詢放在返回結果的方法中,您將遇到相同的問題,因為您無法指定 方法返回什麼。使用 LINQ 的開發人員經常遇到後一種制約。

考慮到匿名類型是不宜重復使用的動態類型這個事實,這些限制雖然令人沮 喪,但還是有些道理。匿名類型不適合在方法之間傳遞。

您需要為預編譯查詢定義符合投影的類型。注意:在實體框架中,您必須使 用類而不是結構,這是因為 LINQ to Entities 不允許投影到沒有構造函數的類 型中。LINQ to SQL 不允許結構成為投影目標。因此,對於實體框架只能使用類 ,而對於 LINQ to SQL,可以使用類或結構以避免匿名類型的相關限制。

預編譯查詢和 LINQ to SQL 預取

預編譯查詢可能出現的另一個問題是預取,又稱作“預先加載”,但此問題 僅存在於 LINQ to SQL 中。在實體框架中,您使用 Include 方法預先加載,這 將導致在數據庫中執行單個查詢。Include 可以是查詢的一部分(如 context.Customer.Include(“Orders”)),因此,此處不存在問題。然而,對 於 LINQ to SQL,預先加載是在 DataContext 而非查詢自身中定義的。

DataContext.LoadOptions 具有 LoadWith 方法,該方法允許您指定哪些相 關數據與特定實體一起預先加載。

可以將 LoadWith 定義為將 Order 與查詢的任何 Customer 一起加載:

Context.LoadOptions.LoadWith<Customer>(c => c.Orders)

然後,可以添加一條規則,指定將詳細信息與任何加載的訂單一起加載:

Context.LoadOptions.LoadWith<Customer>(c =>  c.Orders)
Context.LoadOptions.LoadWith<Order>(o =>o.Details)

您可以針對 DataContext 實例直接定義 LoadOptions 或者創建 DataLoadOptions 類,在此對象中定義 LoadWith 規則,然後將其附加到您的上 下文中:

DataLoadOptions _myLoadOptions = new DataLoadOptions ();
_myLoadOptions.LoadWith<Customer>(c => c.Orders)
Context.LoadOptions= myLoadOptions

對於 LoadOptions 和 DataLoadOptions 類的常規使用,有若干注意事項。 例如,如果您先後定義和附加 DataLoadOptions,則一旦執行針對 DataContext 的查詢後,您將無法附加新的 DataLoadOptions 組。盡管您還有很多加載選項 及其注意事項需要了解,但還是讓我們先看看將一些 LoadOptions 應用到預編 譯查詢的基本模式。

此模式的關鍵是您無需將 DataLoadOptions 與特定上下文關聯即可對其進行 預定義。

在您聲明靜態 Func 變量以包含預編譯查詢的類聲明中,聲明一個新的 DataLoadOptions 變量。務必將此變量也設置為靜態變量,從而使其與委托一起 保持可用:

static DataLoadOptions Load_Customer_Orders_Details = new  DataLoadOptions();

然後,在編譯並調用查詢的方法中,可與委托一起定義 LoadOptions(請參 見圖 2)。此方法對 .NET Framework 3.5 和 .NET Framework 4 有效。

圖 2 與委托一起定義 LoadOptions

public Customer GetRandomCustomerWithOrders()
   {
    if (Load_Customer_Orders_Details == null)
    {
     Load_Customer_Orders_Details = new DataLoadOptions ();
     Load_Customer_Orders_Details.LoadWith<Customer>(c  => c.Orders);
     Load_Customer_Orders_Details.LoadWith<Order>(o =>  o.Details);
    }
    if (_context.LoadOptions == null)
    {
     _context.LoadOptions = Load_Customer_Orders_Details;
    }
    if (_CustWithOrders == null)
    {
     _CustWithOrders =  CompiledQuery.Compile<DataClasses1DataContext, Customer>
         (ctx => ctx.Customers.Where(c =>  c.Orders.Any()).FirstOrDefault());
    }
    return _CustWithOrders.Invoke(_context);
   }

DataLoadOptions 是靜態的,因此僅在需要時進行定義。DataContext 可能 是新的,也可能是舊的,具體取決於類的邏輯。如果是正在重復使用的上下文, 則它擁有之前分配的 LoadOptions。否則,您需要對其進行分配。現在,您可以 反復調用此查詢,並且仍將獲得 LINQ to SQL 的預取功能帶來的好處。

將預編譯放在檢查表的首位

在 LINQ 查詢執行的作用域中,查詢編譯在流程中屬於高成本部分。每次向 基於 LINQ to SQL 或實體框架的應用程序添加 LINQ 查詢邏輯時,您都應考慮 預編譯查詢並重復使用這些查詢。但不要以為您可以從此高枕無憂了。如您所見 ,有些情況下您可能無法享受預編譯查詢帶來的好處。請使用某種類型的探查工 具,例如 SQL 事件探查器或來自 Hibernating Rhinos 的探查工具,包括 L2SProf (l2sprof.com/.com) 和 EFProf (efprof.com)。您可能需要利用此處 所示的部分模式,以確保獲取預編譯查詢本可提供的好處。

下載代碼示 例:http://code.msdn.microsoft.com/mag201003DataPoints

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved