本文章是由機器翻譯。

資料點

以 DDD 設限內容縮小 EF 模型

Julie Lerman

下載代碼示例

Julie Lerman
定義模型,以用於與實體框架 (EF) 時, 開發人員通常包括所有的類,用於整個應用程式。這可能是在 EF 設計器中創建新的資料庫第一個模型和從資料庫中選擇的所有可用表和視圖的結果。對於那些您使用代碼第一次來定義您的模型,這可能意味著在您的類的所有單個 DbCoNtext 創建 DbSet 屬性或者甚至不知情的情況下包括相關的那些已經鎖定了您的類。

當您正在處理一個大型模型和大型應用程式時,有設計更小、 更緊湊的模型,並針對特定的應用程式的任務,而不是具有單一的模式,為整個解決方案的許多好處。在本專欄中,我介紹給你一個概念從域驅動設計 (DDD) — — 綁定上下文 — — 並向您展示如何應用它生成目標的模型與 EF,側重于這樣做與 EF 代碼優先功能的更大的靈活性。如果你是新到 DDD,這一個偉大途徑瞭解即使你不完全致力於 DDD。如果您已經在使用 DDD,您將看到如何在以下 DDD 做法時可以使用 EF 的受益。

領域驅動設計和有界的上下文

DDD 是一個相當大的主題,包括軟體設計的整體視圖。保羅 Rayner,任教 DDD 講習班為域語言 (DomainLanguage.com),蔽:

"DDD 提倡務實、 全面、 連續的軟體設計:域專家要在軟體中嵌入富域模型與合作 — — 模型,説明解決重要的是,複雜的業務問題."

DDD 包括無數的軟體設計模式,其中之一 — — 有界的上下文 — — 完全適合於 EF 與工作。有界範圍內的重點發展目標支援您的業務域中的特定操作的小模型。在他的書,"Domain-Driven 設計"(艾迪生衛斯理 2003年),埃裡克 · 埃文斯解釋有界上下文"分隔特定模型的適用性。定界框的上下文讓團隊成員明確和共同的理解,有什麼要一致和什麼可以獨立發展的"。

較小的模型提供許多好處,允許團隊來定義明確的邊界有關的設計和發展的責任。它們還會導致更好的可維護性 — — 因為上下文具有較小的表面區域,您有較少副作用,不用擔心進行修改時。此外,還有一個性能好處時 EF 創建記憶體中的一個模型的中繼資料,它第一次載入到記憶體時。

因為我正在擴充 EF DbCoNtext 具有有界的上下文,我已經提到我 DbCoNtexts 作為"界 DbCoNtexts"。但是,這兩個不實際等效:DbCoNtext 是一類實現,而樓宇的上下文包括完整的設計進程內大的概念。我因此要參考我的 DBCoNtexts 作為"約束"或"重點"。

比較典型的 EF DbCoNtext 到綁定上下文

DDD 最常應用於大型應用程式開發複雜的業務域中,而較小的應用程式可以也受益于許多及其經驗教訓。為了這一解釋,我將重點針對特定的子域的應用程式:跟蹤銷售和市場行銷的公司。此應用程式中涉及的物件可能範圍從客戶、 訂單和行項,到產品、 行銷、 銷售人員和甚至員工。通常將定義 DbCoNtext 包含需要持久保存到資料庫中,如中所示的解決方案中的每個類的屬性 DbSet 圖 1

圖 1 典型 DbCoNtext 包含在解決方案中的所有域類

public class CompanyContext : DbContext
{
  public DbSet<Customer> Customers { get; set; }
  public DbSet<Employee>  Employees { get; set; }
  public DbSet<SalaryHistory> SalaryHistories { get; set; }
  public DbSet<Order> Orders { get; set; }
  public DbSet<LineItem> LineItems { get; set; }
  public DbSet<Product> Products { get; set; }
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<ShippingAddress> ShippingAddresses { get; set; }
  public DbSet<Payment> Payments { get; set; }
  public DbSet<Category> Categories { get; set; }
  public DbSet<Promotion> Promotions { get; set; }
  public DbSet<Return> Returns { get; set; }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    // Config specifies a 1:0..1 relationship between Customer and ShippingAddress
    modelBuilder.Configurations.Add(new ShippingAddressMap());
  }
}

想像一下如果這是一個更深遠的應用程式與數以百計的類。 您可能還有一些這些類的 Fluent API 配置。 這使得很多要費力和管理單個類中的代碼。 與這種大型的應用程式,可能劃分發展團隊間機構。 此單一的全公司 DbCoNtext,每個團隊將需要代碼的子集基地,超出了他們的責任。 並對這方面的任何團隊的更改可能會影響另一個團隊的工作。

有有趣的問題,您可以對此沒有重點、 首要 DbCoNtext 問自己。 例如,在市場行銷部為目標的應用程式的領域,使用者是否有任何需要與員工薪金歷史資料的工作嗎? 航運部需要訪問相同級別的詳細資訊作為客戶服務代理的客戶嗎? 有人在航運部將需要編輯的客戶記錄嗎? 對於最常見的情況,對這些問題的答案通常將沒有,這可能會説明您查看為什麼它能管理的域物件集較小的幾個 DbCoNtexts 有意義。

航運部重點的 DbCoNtext

由於 DDD 建議與具有明確定義的上下文邊界的更小、 更集中模型的工作,讓我們縮小此 DbCoNtext 針對航運部職能和只有那些執行有關任務所需的類的範圍。 因此,您可以從 DbCoNtext,而只留下那些你需要支援的業務能力與航運有關的刪除 DbSet 的一些屬性。 我拍出的回報、 促銷、 類別、 付款、 員工和 SalaryHistories:

public class ShippingDeptContext : DbContext
{
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<Customer> Customers { get; set; }
  public DbSet<ShippingAddress> ShippingAddresses { get; set; }
  public DbSet<Order> Order { get; set; }
  public DbSet<LineItem> LineItems { get; set; }
  public DbSet<Product> Products { get; set; }
}

EF 代碼第一次使用 ShippingCoNtext 的細節推斷模型。 圖 2 顯示將從該類生成的是使用實體框架電源工具 Beta 2 中創建的模型的視覺化效果。 現在,讓我們開始微調模型。

Visualized Model from First Pass at the ShippingContext
圖 2 視覺化模型從第一通在 ShippingCoNtext

調整 DbCoNtext 和創建目標更集中的類

有仍然更多涉及的類模型中比我指定的航運。 按照約定,代碼第一次包括可到達由其他模型中的類中的所有類。 這就是為什麼類別和付款露面了即使我刪除它們的 DbSet 屬性。 所以我會告訴 DbCoNtext 忽略類別和付款:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Ignore<Category>();
  modelBuilder.Ignore<Payment>();
  modelBuilder.Configurations.Add(new ShippingAddressMap());
}

這可以確保類別和付款不要只顧模型只是因為它們相關產品和秩序。

您可以修改此 DbCoNtext 類更多而不會影響生成的模型。 與這些 DbSet 屬性,它是可能的顯式的這些七集資料在您的應用程式中的每個查詢。 但如果你想想的類以及它們之間的關係,你可能會認為在這方面將永遠不會有必要向查詢航運­直接處理 — — 它始終可以檢索和客戶資料。 即使有沒有 ShippingAddresses DbSet,您可以依靠自動拉在類別和付款拉 ShippingAddress 到模型之間的關係到客戶的同一公約。 所以您可以刪除而不會丟失的資料庫映射到 ShippingAddress 的 ShippingAddresses 屬性。 您可能能夠刪除其他人的理由,但讓我們重點只是這一個:

public class ShippingContext : DbContext
{
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<Customer> Customers { get; set; }
  // Public DbSet<ShippingAddress> ShippingAddresses { get; set; }
  public DbSet<Order> Order { get; set; }
  public DbSet<LineItem> LineItems { get; set; }
  public DbSet<Product> Products { get; set; }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  { ...
}
}

在處理貨件的範圍內,我真的不需要一個完整的客戶物件、 一個完整的命令物件或一個完整的籃物件。 我需要只是產品要裝運、 (從籃) 數量、 客戶的名稱和 ShippingAddress 和向客戶或訂單可能附加的任何注釋。 我有我的資料庫管理員創建的視圖,將返回返款的專案 — — 那些與 ShipmentId = 0 或 null。 同時,我可以定義一個精簡的類將映射到該視圖相關的屬性與我預計需要:

[Table("ItemsToBeShipped")]
public class ItemToBeShipped
{
  [Key]
  public int LineItemId { get; set; }
  public int OrderId { get; set; }
  public int ProductId { get; set; }
  public int OrderQty { get; private set; }
  public OrderShippingDetail OrderShippingDetails { get; set; }
}

處理貨件的邏輯需要查詢的 ItemToBeShipped,然後獲取我也許需要與客戶和 ShippingAddress 無論訂單詳細資訊。 我可以減少我 DbCoNtext 定義,可以讓我查詢開始與這個新的類型和包括訂單、 客戶和航運圖­位址。 但是,因為我知道 EF 將實現這與 SQL 查詢設計拼合結果並返回重複的訂單、 客戶和航運­位址資料以及每個行專案,我會讓程式師查詢的順序,再帶回來一個與客戶和 ShippingAddress 的圖。 但是,再次,我不需要所有的列從訂單表中,因此,我將創建一個類,更好地針對航運部,包括可列印在發貨清單的資訊。 此類是所示的 OrderShippingDetail 類圖 3

圖 3 OrderShippingDetail 類

[Table("Orders")]
public class OrderShippingDetail
{  
  [Key]
  public int OrderId { get; set; }
  public DateTime OrderDate { get; set; }
  public Nullable<DateTime> DueDate { get; set; }
  public string SalesOrderNumber { get; set; }
  public string PurchaseOrderNumber { get; set; }
  public Customer Customer { get; set; }
  public int CustomerId { get; set; }
  public string Comment { get; set; }
  public ICollection<ItemToBeShipped> OpenLineItems { get; set; }
}

請注意我 ItemToBeShipped 類具有一個導覽屬性為 OrderShippingDetail 和 OrderShippingDetail 有一個用於客戶。 導覽屬性將幫我查詢和保存時的圖。

有一個更多片這個謎團。 航運部將需要表示的專案,如發運和 LineItems 表具有 ShipmentId 的列,用來綁到裝運行專案。 該應用程式將需要更新 ShipmentId 欄位專案發貨時。 我將創建一個簡單的類,來照顧這項任務,而不是依靠籃類,用於進行銷售:

[Table("LineItems")]
public class LineItemShipment
{
  [Key]
  public int LineItemId { get; set; }
  public int ShipmentId { get; set; }
}

每當一個專案已經裝運,可以使用的適當的值創建此類的一個新實例並強制應用程式更新資料庫中的籃。 它將重要來設計您的應用程式類只用于此目的。 如果您嘗試插入使用此類而不一個該帳戶作為非可以為 null 的表字段,如訂單 id 籃,資料庫將引發異常。

一些更多的微調之後, 我 ShippingCoNtext 現在定義為:

public class ShippingContext : DbContext
{
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<OrderShippingDetail> Order { get; set; }
  public DbSet<ItemToBeShipped> ItemsToBeShipped { get; set; }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    modelBuilder.Ignore<LineItem>();
    modelBuilder.Ignore<Order>();
    modelBuilder.Configurations.Add(new ShippingAddressMap());
  }
}

使用實體框架電源工具 Beta 2 再次創建 EDMX,我可以看到在模型瀏覽器視窗中 (圖 4) 代碼第一推斷該模型包含指定的 DbSets,以及客戶和 ShippingAddress,其中發現了通過導覽屬性從 OrderShippingDetail 類的四類。

Model Browser View of ShippingContext Entities as Inferred by Code First
圖 4 ShippingCoNtext 實體作為首先推斷由代碼模型瀏覽器視圖

重點突出的 DbCoNtext 和資料庫初始化

使用較小的 DbCoNtext 支援在應用程式中的有界的具體情況,時,關鍵的是要記住兩個 EF 代碼優先預設行為對資料庫初始化。

第一個是上下文的代碼第一次將會尋找一個資料庫的名稱。 當您的應用程式具有 ShippingCoNtext、 CustomerCoNtext、 SalesCoNtext 和其他人,這是不可取。 相反,您希望所有的 DbCoNtexts,以指向相同的資料庫。

要考慮的第二個預設行為是代碼第一次將使用由 DbCoNtext 推斷的模型來定義資料庫架構。 但現在你擁有 DbCoNtext 表示只有一個資料庫的切片。 為此,你不想要觸發資料庫初始化的 DbCoNtext 類。

它是可以解決這兩個問題中的每個上下文類建構函式。 例如,這裡的 ShippingCoNtext 類中可以讓你指定 DPSalesDatabase 和禁用資料庫初始化的建構函式:

public ShippingContext() : base("DPSalesDatabase")
{
  Database.SetInitializer<ShippingContext>(null);
}

但是,如果您在您的應用程式中有大量 DbCoNtext 類,這將成為維護的問題。 更好的模式是以指定一個基類,禁用資料庫初始化,並在同一時間設置的資料庫:

public class BaseContext<TContext>
  DbContext where TContext : DbContext
{
  static BaseContext()
  {
    Database.SetInitializer<TContext>(null);
  }
  protected BaseContext() : base("DPSalesDatabase")
  {}
}

現在我各種的上下文類,可以實現 BaseCoNtext 而不是每個都有其自己的建構函式:

public class ShippingContext:BaseContext<ShippingContext>

如果你正在做的新發展和您想要讓代碼首先創建或遷移資料庫基於您的類,您需要"前衛-使用創建模型"DbCoNtext 包含所有的類和關係建立一個表示資料庫的完整模型所需。 但是,這種情況下必須從 BaseCoNtext 不繼承。 當您對您的類結構進行更改時,您可以運行一些代碼,使用 uber 上下文來執行資料庫初始化,是否你創建或遷移資料庫。

行使重點的 DbCoNtext

所有的這地方,我創建了一些自動化的集成測試,以執行以下任務:

  • 檢索打開的行專案。
  • 檢索 OrderShippingDetails 以及客戶和航運返款行專案的訂單資料。
  • 檢索返款的行專案,並創建新的裝運。 設置為行專案的裝運和更新資料庫中的行專案具有新的裝運鍵值時向資料庫中插入新的托運。

這些都是最關鍵的功能,您將需要執行那些已下令他們客戶的航運產品。 所有測試均都通過,驗證我 DbCoNtext 按預期方式工作。 在這篇文章的示例下載中包括的測試。

總結

我不只創建了 DbCoNtext,專門側重支援航運的任務,但思考它還説明我創造更高效的域物件,用於執行這些任務。 使用 DbCoNtext 來對齊我有界與我以這種方式的航運子網域內容意味著別要涉水通過泥潭的代碼上的航運部功能的應用程式的工作,我可以做什麼我需要到專門的域物件而不會影響其他領域的發展努力。

您可以看到其他示例聚焦 DbCoNtext 使用的綁定上下文在書中,DDD 概念的"程式設計實體框架:DbCoNtext"(O'Reilly 媒體,2011年),其中我與羅恩 · 米勒合著。

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

由於以下的技術專家對本文的審閱:保羅 Rayner