資料點

EF 模型中基底記錄類別的陷阱與指標

Julie Lerman

 

Julie Lerman
我最近遇到的一些偶然的客戶與花時間 — — 但嚴重 — — 他們實體框架相關的代碼的性能問題。我們使用一種工具來設定檔由實體框架生成的查詢,發現打資料庫 5,800 線 SQL 查詢。(在瞭解性能分析工具在我 2010 年 12 月的資料點的列中,"分析資料庫活動的實體框架中," msdn.microsoft.com/magazine/gg490349。)我喘著氣說看見的 EDMX 模型包含教了朋友、 親人和開發人員,以避免繼承層次結構。模型有一個單一的每個其他實體從中派生的基地實體。基實體被用來確保每個實體都要跟蹤日誌記錄的資料,如創建日期 – 和 DateLastModified 的屬性。因為這種模式使用模型首次創建的繼承被視為實體框架的每個類型 (TPT) 模型中的每個實體映射到其自己的表在資料庫中的表。對於新手來說,這看起來不夠無辜。

但 TPT 繼承是惡名昭彰的實體框架中為其通用的查詢生成模式,會導致龐大、 業績糟糕的 SQL 查詢。您可能已開始與凡可以避免 TPT,或您可能已經陷入與現有的模型和 TPT 繼承的一種新的模式。不管怎樣,本月的專欄致力於説明您理解潛在的性能缺陷的血栓前體蛋白在此方案中,並向您顯示幾個花招您可以利用它們繞過他們如果要修改的模型和資料庫的架構已經太晚。

可怕的模型

圖 1 顯示模型的示例,看到了所有過於頻繁。注意到名為 TheBaseType 的實體。每個其他實體源于它為了自動繼承創建日期 – 屬性。我明白為什麼它很容易設計它這種方式,但實體框架架構規則還要求的基類型擁有每個派生實體的鍵屬性。對我來說,這已經是信號不適當使用繼承的紅旗。

All Classes Inherit from TheBaseType
圖 1 從 TheBaseType 中的所有類都繼承

它不是專門創建一個問題這一設計 ; 該實體框架 這是在模型本身的設計缺陷。在這種情況下,繼承表示客戶是 TheBaseType。如果我們該基地的實體的名稱更改為"LoggingInfo",然後重複該語句"客戶是 LoggingInfo"嗎?該語句的謬誤變得更明顯的新的類名。比較對客戶是一個人。也許我現在深信你要避免在你的模型中這樣做。但如果不是,或者如果你已經被堵使用這一模型,讓它幾乎沒有進一步。

預設情況下,該模型第一個工作流定義與基礎資料表和所有代表的派生的類型的表之間的一對一關聯性的資料庫架構。這是前面提到的血栓前體蛋白層次結構。

如果您要執行幾個簡單的查詢,您可能沒有注意任何問題 — — 特別是如果,像我一樣,你不是 DBA 或其他資料庫大師的味道。

例如,此 LINQ to 實體查詢檢索的創建日期 – 屬性為某個特定的客戶:

 

context.TheBaseTypes.OfType<Customer>()
  .Where(b => b.Id == 3)
  .Select(c => c.DateCreated)
  .FirstOrDefault();

在資料庫中執行的以下 TSQL 中的查詢結果:

    SELECT TOP (1)
    [Extent1].[DateCreated] AS [DateCreated]
    FROM  [dbo].[TheBaseTypes] AS [Extent1]
    INNER JOIN [dbo].[TheBaseTypes_Customer] 
    AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
    WHERE 3 = [Extent1].[Id]

它是一個非常好的查詢。

查詢以檢索整個實體有點醜,這是執行嵌套的查詢的需要的。 基查詢檢索的所有代表 TheBaseTypes 表和包含派生的類型的表之間的聯接的欄位。 然後查詢這些結果專案要返回到實體框架來填充類型的欄位。 例如,下面是一個查詢來檢索一個單一的產品:

 

context.TheBaseTypes.OfType<Product>().FirstOrDefault();

圖 2 顯示在伺服器上執行的 TSQL。

嵌套的 TSQL 查詢時指定一種類型的圖 2 部分清單

    SELECT
    [Limit1].[Id] AS [Id],
    [Limit1].[C1] AS [C1],
    [Limit1].[DateCreated] AS [DateCreated],
    [Limit1].[ProductID] AS [ProductID],
    [Limit1].[Name] AS [Name],
    [...continued list of fields required for Product class...]
    FROM ( SELECT TOP (1)
          [Extent1].[Id] AS [Id],
          [Extent1].[DateCreated] AS [DateCreated],
          [Extent2].[ProductID] AS [ProductID],
          [Extent2].[Name] AS [Name],
          [Extent2].[ProductNumber] AS [ProductNumber],
          [...continued list of fields from Products table aka "Extent2" ...],
          [Extent2].[ProductPhoto_Id] AS [ProductPhoto_Id],
          '0X0X' AS [C1]
          FROM  [dbo].[TheBaseTypes] AS [Extent1]
          INNER JOIN [dbo].[TheBaseTypes_Product] 
          AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
    )  AS [Limit1]

還不是這種壞的查詢。 如果你被分析您在該點的查詢,您可能沒有注意引起的模型設計的任何問題。

但這下一步的"簡單"查詢,想要找到所有關于哪些物件,創建了今天,無論何種類型? 在您從基地派生的模型中,查詢可以返回客戶、 產品、 訂單、 雇員或其他任何類型。 模型設計,這似乎是一個合理的要求,和模型加 LINQ to 實體可以方便地表達 (創建日期 – 存儲在資料庫中為日期的類型,所以我不一定要關注到在我的依例查詢中的日期時間欄位比較):

var today= DateTime.Now.Date;
context.TheBaseTypes
  .Where(b => b.DateCreated == today)  .ToList();

表示此查詢中的 LINQ to 實體是簡短甜蜜。 但是,不要被矇騙了。 它是一個龐大的請求。 你問 EF 和您的資料庫返回任何類型的實例 (無論是客戶或產品或雇員) 今日 (星期三) 創建。 實體框架必須通過查詢到派生實體映射的每個表並加入到單個相關 TheBaseTypes 表中創建日期 – 欄位,每一個開始。 在我的環境中,這將創建一個 3,200 線查詢 (當通過 EFProfiler 來很好地格式化該查詢),就能使實體框架一些時間生成和資料庫一些時間來執行。

以我的經驗,像這樣的查詢的反正所屬業務分析工具中。 但如果你的模型,並且您想要獲得該資訊在您的應用程式,也許您正在構建應用程式的管理報告區域嗎? 我見過開發商嘗試去做這種類型的查詢在其應用程式中,與我還是得說你需要思考實體框架框。 構建邏輯到資料庫中的視圖或存儲的過程作為和從實體框架調用,而不問 EF 為您生成查詢。 即使作為一個資料庫過程,此特定的邏輯並不簡單打造。 但有很多好處。 第一,你有更好的機會建立一個業績較好的查詢。 第二,EF 不必花時間去弄清楚該查詢。 第三,您的應用程式不需要發送 3,300 (或更多 !)-跨管道路線查詢。 不過需要提醒的更深入探討這一問題,並嘗試解決它從資料庫內或通過使用 EF 和.net 中編碼的邏輯,清晰它會成為問題是沒有那麼多實體框架作為整體模型的設計,讓你的方式。

如果您可以避免從的基類型和查詢特定類型查詢,您的查詢將會變得更為簡單。 這裡是一個表示焦點到前面的查詢特定類型的示例:

context.TheBaseTypes.TypeOf<Product>()
  .Where(b => b.DateCreated == today)
  .ToList();

因為 EF 沒有準備在模型中的每個類型,由此產生的 TSQL 是 25 線的簡單查詢。 與 DbCoNtext API,您甚至不必使用 TypeOf 到派生的查詢類型。 很可能要創建派生類型的 DbSet 屬性。 所以更多隻是可以查詢了:

context.Products
  .Where(b => b.DateCreated == today)
  .ToList();

其實,這種模式我會完全從我上下文類刪除 TheBaseTypes DbSet,防止任何人表達直接從該基類型的查詢。

日誌記錄不可怕的模型

我將重點放到目前為止在我強烈建議開發 opers 建築物實體框架模型時,為了避免一個層次結構方案:作為基地派生自的每一個單一的實體模型中還使用映射的一個實體。 有時我來時情形哪裡就只是太晚來更改模型。 但其他時間我能夠説明我的客戶完全避免此路徑 (或它們足夠早在我們能夠改變模型的發展)。

那麼,如何更好地實現目標 — — 這是通常提供跟蹤資料如日誌記錄資料 — — 在你的模型中的每個類型的呢?

通常情況下,第一個念頭是保持但是更改繼承層次結構中的類型。 模型第一、 血栓前體蛋白是預設但您可以更改此到表中每個層次結構 (TPH) 使用實體設計器生成電源包 (在 Visual Studio 庫通過功能擴展管理器中可用)。 當您在您的類中定義繼承時,代碼首先預設值為 TPH。 但你很快就會認為這不是解決方案在所有。 為什麼? 碳氫化合物是指整個層次結構包含在單個表。 換句話說,您的資料庫將包括只是一個表。 我希望沒有更多的解釋必要說服你這不是一個很好的路徑。

正如我較早前說 (當我問是否客戶真的是一種類型的 LoggingInfo),特定情形中,我將重點放在,以解決問題的跟蹤常見的資料,回避你只是 inher itance 完全避免以實現這個目標。 我會建議你考慮一個介面或複雜類型相反,其中將嵌入到每個表的欄位。 如果你已經背負著創建一個單獨的表的資料庫,將會做一種關係。

為了演示,我將切換到基於類使用代碼第一次而不 EDMX (雖然您可以實現這些同樣的模式,使用 EDMX 模型和設計器) 的模型。

在首宗案件中,我將使用一個介面:

public interface ITheBaseType
{
  DateTime DateCreated { get; set; }
}

每個類將實現該介面。 它將有它自己的關鍵屬性,並包含創建日期 – 屬性。 例如,下面是產品類別:

public class Product : ITheBaseType
{
  public int ProductID { get; set; }
  // ...other properties...
  public DateTime DateCreated { get; set; }
}

在資料庫中,每個表具有其自己的創建日期 – 屬性。 因此,重複早些時候針對產品查詢創建一個簡單的查詢:

context.Products
.Where(b => b.DateCreated == today)
.ToList();

因為所有的欄位都包含此表中,我不再需要嵌套的查詢:

    SELECT TOP (1) [Extent1].[Id]                     AS [Id],
                   [Extent1].[ProductID]              AS [ProductID],
                   [Extent1].[Name]                   AS [Name],
                   [Extent1].[ProductNumber]          AS [ProductNumber],
                   ...more fields from Products table...
                   [Extent1].[ProductPhoto_Id]        AS [ProductPhoto_Id],
                   [Extent1].[DateCreated]            AS [DateCreated]
    FROM   [dbo].[Products] AS [Extent1]
    WHERE  [Extent1].[DateCreated] = '2012-05-25T00:00:00.00'

如果您希望定義一個複雜類型和重用,在每個類中,您的類型可能類似于:

public class Logging
{
  public DateTime DateCreated { get; set; }
}
public class Product{
  public int ProductID { get; set; }
  // ...other properties...
  public Logging Logging { get; set; } 
}

請注意日誌記錄類沒有鍵欄位 (如 Id 或 LoggingId)。 代碼第一次約定將假定這是一個複雜的類型和它用於定義屬性在其他類中,像我剛做的產品時也這樣對待它。

在資料庫中的產品表包含生成的代碼第一個稱為 Logging_DateCreated 和 Product.Logging.DateCreated 屬性對應到的列的列。 向客戶類中添加日誌記錄屬性將具有相同的效力。 客戶表也將自己的 Logging_DateCreated 屬性,和它映射回 Customer.Logging.DateCreated。

在代碼中,您需要流覽日誌記錄屬性來引用該創建日期 – 欄位。 這裡是相同的查詢之前,重寫以使用新類型:

context.Products.Where(b => b.Logging.DateCreated == DateTime.Now).ToList();

產生的 SQL 是介面示例相同的欄位的名稱現在是 Logging_DateCreated,而不是創建日期 – 除外。 它是一個短的查詢,僅查詢產品表。

繼承的類中的原始模型的優點之一是它易於代碼邏輯來自動填滿的欄位從基類 — — 在 SaveChanges,例如期間。 但是,您可以創建邏輯,一樣輕鬆地為複雜類型或介面,所以我不覺得這些新的模式的任何不利之處。 圖 3 顯示了一個簡單的示例,在 SaveChanges (您可以學習更多關於這種技術我"程式設計實體框架"叢書的第二次和 DbCoNtext 版) 過程中設置新的實體的創建日期 – 屬性。

圖 3 在 SaveChanges 期間設置介面的創建日期 – 屬性

public override int SaveChanges()
{
  foreach (var entity in this.ChangeTracker.Entries()
    .Where(e =>
    e.State == EntityState.Added))
  {
    ApplyLoggingData(entity);
  }
  return base.SaveChanges();
}
private static void ApplyLoggingData(DbEntityEntry entityEntry)
{
  var logger = entityEntry.Entity as ITheBaseType;
  if (logger == null) return;
  logger.DateCreated = System.DateTime.Now;
}

EF 5 的某些變化

實體框架 5 不會帶來一些改善 TPT 層次結構的説明,但不是會減輕我前面所示的問題從生成的查詢。 例如,重新運行我最初導致了 Microsoft.net 框架 4.5 安裝 (與任何其他更改到解決方案) 的電腦上的 SQL 3,300 行的查詢會生成減少到 2,100 行的 SQL 查詢。 其中一個最大的不同是 EF 5 不依賴于工會生成查詢。 我不是資料庫管理員,但我的理解是這種改進不會影響資料庫中查詢的性能。 你可以閱讀更多有關此更改為 TPT 查詢在我博客中"實體框架 CTP 2011 年 6 月:TPT 繼承查詢的改進,"在 bit.ly/MDSQuB

並不是所有繼承都是邪惡

在你的模型中有單一的基類型的所有實體是一個極端的例子,建模和繼承出毛病。 有很多好情況下,在你的模型中有繼承層次結構 — — 例如,當您想描述客戶是一個人。 同樣重要的是 LINQ to 實體是只是一個工具,為您提供一課。 在方案中我的客戶告訴我,聰明的資料庫開發人員重建針對基類型的欄位的查詢作為資料庫預存程序,使 multisecond 一拿只 9 毫秒為單位) 的活動。 我們都歡呼起來。 我們仍然希望他們將能夠重新設計模型,雖然調整的下一個版本的軟體,該資料庫。 在此期間,他們能夠讓實體框架繼續生成這些並不是有問題的查詢和使用的工具,我留下來調整應用程式和資料庫的一些很棒的性能改進。

Julie Lerman 是 Microsoft MVP,.net 的導師和顧問住在佛蒙特州的丘陵。您可以找到她介紹資料訪問和使用者組和世界各地的會議其他 Microsoft.net 主題。在她博客 thedatafarm.com/blog 和的作者是"程式設計實體框架"(2010 年) (2011 年) 的代碼第一版和 DbCoNtext 版 (到 2012 年),所有從 O'Reilly 媒體。跟她在 Twitter 上 twitter.com/julielerman

由於下面的技術專家,檢討這篇文章:迭戈維加