2016 年 11 月

第 31 卷,第 11 期

本文章是由機器翻譯。

資料點 - CQRS 與 EF 資料模型

Julie Lerman

Julie Lerman命令查詢責任隔離 (CQRS) 是一種模式,基本上是提供指引分隔讀取資料與變更導致系統的狀態 (例如,傳送一則確認訊息或寫入至資料庫),並據此設計物件和架構的責任。它最初被用來協助高度交易系統,例如,銀行。Greg Young CQRS 演變小姐 Meyer 命令查詢分離 (CQS) 策略,其最有價值的概念,根據 Martin Fowler 」 是時是很方便您可以明顯不同於不變更狀態的方法 」 (bit.ly/2cuoVeX)。CQRS 的加入是建立完全不同的命令與查詢的模型的概念。

CQRS 通常已經投入貯體不正確地為特定類型的架構,或 Domain-Driven 設計的一部分或訊息或事件。在 2010年部落格文章中,「 CQRS、 工作為基礎的 Ui 中,事件 Sourcing,Agh ! 」(bit.ly/1fZwJ0L),Young 說明的 CQRS 是沒有任何這些動作,但只可以協助進行架構決策的模式。CQRS 其實是 「 有兩個物件已經先前只有一個。 」 不過它的確套用的軟體的部分並非專屬於資料模型或服務界限。事實上,他說明 「 最大的好處不過是它會辨識其 (願) 時,都是不同的架構屬性處理命令和查詢。

在定義資料模型 (通常與 Entity Framework [EF]),我已經粉絲運用這種模式 — 在特定案例。如往常,我的想法做為指引,非規則,就像我選擇套用 CQRS 幫助我達到我的架構的方式,我希望您會讓它們圖形以符合您自己的特定需求。

處理與 EF 的關聯性的優點

Entity Framework 在執行階段很容易讓使用關聯性。查詢時,這是一大優勢。實體之間的關聯性,可讓您查詢時,瀏覽這些關聯性。從資料庫擷取相關的資料既簡單又有效率。您可以選擇立即載入與 Include 方法或投影,之後事實消極式載入或之後事實明確式載入。這些功能並未改變許多 EF 的原始版本後,也因為我撰寫其相關年代 2011 年 6 月 」 Demystifying Entity Framework 策略︰ 載入相關的資料 」 (msdn.com/magazine/hh205756)。

在模型中的標準範例 [圖 1 進行簡單查詢,以顯示客戶的訂單的詳細資訊,明細項目和產品名稱] 頁面上。您可以撰寫有效率的查詢如下︰

var customersWithOrders = context.Customers
  .Include(c => c.Orders.Select(
  o => o.LineItems.Select(p => p.Product)))
  .ToList();

Entity Framework 資料模型使用緊密結合的關聯性
[圖 1 緊密結合的關聯性與 Entity Framework 資料模型

EF 會將此轉換會擷取所有相關資料都在一個資料庫命令的 SQL。然後,從結果中,EF 會具體化的客戶與其訂單、 訂單行項目,即使每一行項產品詳細資料的完整圖形。

它的確可以讓填入如視窗 Presentation Foundation (WPF)] 視窗中的頁面 [圖 2 簡單。我可以在一行程式碼︰

customerViewSource.Source = customersWithOrders

資料控制項繫結至單一物件圖形
[圖 2 資料控制項繫結至單一物件圖形

以下是開發人員愛用的另一個好處︰ 建立時的圖形,EF 運作後或等至資料庫,才能插入父代,傳回新的主索引鍵值,然後套用所做的外部索引鍵值為項目子系再建置及執行其 insert 命令。

它是所有很神奇。Magic 有其缺點,但 EF 資料模型,在魔力來自有密切的關聯性可能會導致副作用來執行更新,甚至使用查詢的時候。值得注意的副作用可以發生在參考資料附加到新的記錄,使用導覽屬性,並且再呼叫 SaveChanges。例如,您可能會建立新的明細項目並設定其產品屬性來自資料庫的現有產品的執行個體。在已連線的應用程式,例如 WPF 應用程式,其中 EF 可能會追蹤每一項變更其物件,EF 會確認產品已預先存在。但在 EF 開始進行變更之後,才追蹤物件已中斷連線的情況下,EF 會假設新產品,像是行項目,並會將它插入資料庫中一次。當然也有這些問題的因應措施。對於這個問題,我總是建議設定的外部索引鍵值 (ProductId) 而非執行個體。另外還有如何追蹤狀態與周旋搭配 EF 之前儲存的資料。事實上,我最近的專欄中,「 處理狀態的中斷連線的實體在 EF 」 (msdn.com/magazine/mt694083),顯示這樣的模式。

以下是另一個常見的陷阱︰ 所需的導覽屬性。根據您如何互動的物件,您可能不在意的導覽屬性 — 但 EF 肯定會發現它是否遺失。我撰寫了這種類型的另一個資料行 」 進行請勿使用不存在外部索引鍵 > 中的問題 (msdn.com/magazine/hh708747)。

因此,的確會因應措施。但是,您也可以利用 CQRS 模式,以建立較為簡潔且更明確的 Api,不需要因應措施。這也表示,將會更容易維護且較不需要額外的副作用。

CQRS 模式套用 DbContext 和網域類別

我已經常用 CQRS 模式來幫助我解決這個問題。授與這的確意味著無論模型分解成會導致兩次許多類別 (雖然不一定是兩倍的程式碼)。不只建立兩個不同的 DbContexts,但通常我會將網域類別的組,每個著重於解決讀取或寫入相關的工作。

我使用我稍有不同的模型會形成比較簡單的一個已顯示的範例。這個範例來自我建置了新的 Pluralsight 課程的可調整大小的解決方案。在模型中,沒有 SalesOrder 類別,做為彙總的根網域中。換句話說,SalesOrder 型別引數控制至任何其他相關型別在彙總 — 它會控制如何建立命令列項目、 如何計算折扣,送貨地址方式衍生,依此類推。如果您認為我剛剛提到之工作相關,它們被著重在順序建立。您真的不需要擔心的規則建立新的訂單明細項目,當您只要從資料庫讀取訂單資訊。

相反地,檢視資料時,可能有很多有趣的資訊,瞭解比我只將資料發送到資料庫時,我想知道。

查詢資料的模型

[圖 3 SalesOrder 類型會顯示我的解決方案 Order.Read.Domain 專案中。有很多這裡的屬性,且只有一個方法來建立更佳顯示資料。您沒有看到在這裡的商務規則,因為不必擔心資料驗證。

[圖 3 SalesOrder 型別定義用於資料讀取

namespace Order.Read.Domain {
 public class SalesOrder : Entity  {
  protected SalesOrder()   {
    LineItems = new List<LineItem>();
  }
  public DateTime OrderDate { get; set; }
  public DateTime? DueDate { get; set; }
  public bool OnlineOrder { get; set; }
  public string PurchaseOrderNumber { get; set; }
  public string Comment { get; set; }
  public int PromotionId { get; set; }
  public Address ShippingAddress { get; set; }
  public CustomerStatus CurrentCustomerStatus { get; set; }
  public double Discount   {
    get { return CustomerDiscount + PromoDiscount; }
  }
  public double CustomerDiscount { get; set; }
  public double PromoDiscount { get; set; }
  public string SalesOrderNumber { get; set; }
  public int CustomerId { get; set; }
  public double SubTotal { get; set; }
  public ICollection<LineItem> LineItems { get; set; }
  public decimal CalculateShippingCost()   {
    // Items, quantity, price, discounts, total weight of item
    // This is the job of a microservice we can call out to
    throw new NotImplementedException();
  }
}

比較一下這在 SalesOrder [圖 4, ,這我所定義的情況下,我將會在其中儲存到資料庫的 SalesOrder 資料 — 它是新的訂單,或我正在編輯的其中一個。在這個版本中沒有更多的商務邏輯。沒有的 factory 方法,以及確保未使用的特定資料,無法建立訂單的私用和受保護建構函式。如何建立新的明細項目,訂單,以及如何套用送貨地址,則需要有與邏輯和規則的方法。沒有方法用來控制如何及何時可以修改一組特定的訂單詳細資料。

[圖 4 SalesOrder 類型的建立和更新資料

namespace Order.Write.Domain {
  public class SalesOrder : Entity   {
    private readonly Customer _customer;
    private readonly List<LineItem> _lineItems;
    public static SalesOrder Create(IEnumerable<CartItem>
      cartItems, Customer customer) {
      var order = new SalesOrder(cartItems, customer);
      return order;
    }
    private SalesOrder(IEnumerable<CartItem> cartItems, Customer customer) : this(){
      Id = Guid.NewGuid();
      _customer = customer;
      CustomerId = customer.CustomerId;
      SetShippingAddress(customer.PrimaryAddress);
      ApplyCustomerStatusDiscount();
      foreach (var item in cartItems)
      {
        CreateLineItem(item.ProductId, (double) item.Price, item.Quantity);
      }
      _customer = customer;
    }
    protected SalesOrder() {
      _lineItems = new List<LineItem>();
      Id = Guid.NewGuid();
      OrderDate = DateTime.Now;
    }
    public DateTime OrderDate { get; private set; }
    public DateTime? DueDate { get; private set; }
    public bool OnlineOrder { get; private set; }
    public string PurchaseOrderNumber { get; private set; }
    public string Comment { get; private set; }
    public int PromotionId { get; private set; }
    public Address ShippingAddress { get; private set; }
    public CustomerStatus CurrentCustomerStatus { get; private set; }
    public double Discount{
      get { return CustomerDiscount + PromoDiscount; }
    }
    public double CustomerDiscount { get; private set; }
    public double PromoDiscount { get; private set; }
    public string SalesOrderNumber { get; private set; }
    public int CustomerId { get; private set; }
    public double SubTotal { get; private set; }
    public ICollection<LineItem> LineItems  {
      get { return _lineItems; }
    }
    public void CreateLineItem(int productId, double listPrice, int quantity)
    {
      // NOTE: more rules to be implemented here
      var item = LineItem.Create(Id, productId, quantity, listPrice,
        CustomerDiscount + PromoDiscount);
      _lineItems.Add(item);
    }
    public void SetShippingAddress(Address address) {
      ShippingAddress = Address.Create(address.Street, address.City,
        address.StateProvince, address.PostalCode);
    }
    public bool HasLineItems(){
      return LineItems.Any();
    }
    public decimal CalculateShippingCost() {
      // Items, quantity, price, discounts, total weight of item
      // This is the job of a microservice we can call out to
      throw new NotImplementedException();
    }
    public void ApplyCustomerStatusDiscount() {
      // The guts of this method are in the sample
    }
    public void SetOrderDetails(bool onLineOrder,
      string PONumber, string comment, int promotionId, double promoDiscount){
      OnlineOrder = onLineOrder;
      PurchaseOrderNumber = PONumber;
      Comment = comment;
      PromotionId = promotionId;
      PromoDiscount = promoDiscount;
    }
  }
}

SalesOrder 的寫入版本是更複雜。但如果我需要讀取的版本上運作,不會有所有該無關的撰寫邏輯以我的方式。如果您的可讀取的程式碼會是較不容易發生錯誤的程式碼的指引,您可能,跟我一樣,必須為偏好這種區隔的另一個原因。一定有人像 Young 會認為即使此類別中有太多邏輯。但對我們來說,這會執行。

CQRS 模式可讓我的重點在於填入 SalesOrder (即,在此情況下,幾個) 並定義類別時,分別建立 salesorder 時的問題的問題。這些類別沒有一些共通。比方說,這兩個版本的 SalesOrder 類別會定義成 ICollection < 清單 > 屬性的明細項目類型的關係。

現在讓我們看看他們的資料模型。也就是 DbContext 類別我會使用資料存取。

OrderReadContext 定義單一 DbSet,也就是 SalesOrder 實體︰

public DbSet<SalesOrder> Orders { get; set; }

EF 探索相關的明細項目類型和建立模型所示 [圖 5。不過,由於 EF 需要公開 DbSet,它也可讓任何人呼叫 OrderReadContext.SaveChanges。這是層級的朋友。Andrea Saltarello 提供絕佳的方法,將封裝 DbContext,以便只 DbSet 公開和開發人員 (或未來您) 使用此類別不能直接存取 OrderReadContext。這可協助避免意外讀取模型上呼叫 SaveChanges。

OrderReadContext 為基礎的資料模型
[圖 5 OrderReadContext 為基礎的資料模型

這種類別的簡單範例如下︰

public class ReadModel {
  private OrderReadContext readContext = null;
  public ReadModel() {
    readContext = new OrderReadContext();
  }
  public IQueryable<SalesOrder> Orders {
    get {
      return readContext.Orders;
    }
  }
}

您可以將此實作中加入另一個保護是事實的利用 SaveChanges 是事實的虛擬。您可以覆寫 SaveChanges,使其永遠不會呼叫內部 DbContext.SaveChanges 方法。

OrderWriteContext 定義兩個 DbSets︰ 不只是一個 SalesOrder,但另一個用於 LineItem 實體︰

public DbSet<SalesOrder> Orders { get; set; }
public DbSet<LineItem> LineItems { get; set; }

已經,很有趣,因為我懶得公開 DbSet 的命令列中之其他 DbContext 的項目。在 OrderReadContext,我將查詢只能透過 SalesOrders。我不會過查詢直接針對命令列的項目,因此不需要將該類型的 DbSet。請記住,查詢來填入中的 WPF 視窗 [圖 2。我立即載入訂單 DbSet 透過命令列項目。

OrderWriteContext 中的其他重要邏輯是我已明確告知 EF 來忽略 SalesOrder 使用 fluent API 的明細項目之間的關聯性︰

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
  modelBuilder.Entity<SalesOrder>().Ignore(s => s.LineItems);
}

所產生之模型看起來像 [圖 6

OrderWriteContext 為基礎的資料模型
[圖 6 OrderWriteContext 為基礎的資料模型

這表示我無法使用 EF 從瀏覽至 SalesOrder 明細項目。它並不會防止我在我的商務邏輯。如您所見,我將在命令列項目進行互動的 SalesOrder 類別中有很多程式碼。但我無法寫入命令列項目,像是內容巡覽的查詢。SalesOrders.Include (s = > s.LineItems)。直到我提醒您,這是模型,以便將資料寫入,而不在閱讀本文後,可能會引發異常事件的時間。EF 可以擷取與使用 OrderReadContext 沒問題的相關的資料。

寫入的無關聯性的 DbContext 的優缺點

因此什麼我得到寫入責任,從查詢責任分隔? 它是可以輕鬆地查看缺點。我有多個程式碼來維護。更重要的是,EF 將不會神奇地更新圖形我。我得有更多的工作,以手動方式以確保,當我在插入、 更新或刪除資料,會正確處理關聯性。例如,如果您有程式碼,將新的明細項目加入至 SalesOrder,單靠撰寫 myOrder.LineItems.Add(someItem) 就不會觸發 EF orderId 推入明細項目,將明細項目保存資料庫的時候。您必須明確地設定該訂單編號的值。如果您回頭看在 SalesOrder 的 CreateLineItem 方法 [圖 4, ,您會看到我把它涵蓋。在我的系統,建立新的訂單行項目唯一的方法是透過該非常的方法,這表示我無法撰寫的程式碼在其他地方遺漏該套用 orderId 項重要步驟。您可能會問的另一個問題是︰ 「 如果我想要變更的特定項目,將 orderId? 」 我的系統中,這是一個不太懂的有意義的動作。我可以看到移除訂單明細項目。我可以看到列項目加入訂單。但沒有商務規則,允許變更訂單編號。不過,我不能幫助想要這些 「 什麼 ifs 「 因為我因此用於只在我的資料模型中建置這些功能。

除了明確的控制項,我有的關聯性,即時讀取和寫入邏輯也會取得我想到的所有邏輯新增至我的資料模型依預設,當部分不會使用邏輯。與該沒有直接關聯邏輯可能會強制我撰寫因應措施以避免其副作用。

問題我帶出稍早關於重新新增到資料庫不小心參考資料,或 null 值,當您正在閱讀您不想要更新的資料引入的 — 這些問題也將會消失。讀取定義的類別可能包含您想要看到更新的值。我 SalesOrder 範例沒有這種問題。但無法避免寫入類別,包括您可能想要檢視但不是更新,因此,避免覆寫的屬性會忽略具有 null 值的屬性。

請確定它是獲益良多

CQRS 可以加入您系統的開發工作。請務必看看當 CQRS 可能只是對正在解決的問題,例如在 Udi Dahan 所提供的指引的文章 bit.ly/2bIbd7i。Dino Esposito 的 「 CQRS 的通用應用程式 」 (msdn.com/magazine/mt147237) 也會提供深入了解。我使用特定此模式不是什麼您可以將視為成熟的 CQRS,但會提供 「 使用權限 」 分割讀取和寫入 CQRS 幫助我降低 overreaching 的資料模型必須取得方式的解決方案的複雜性。尋找平衡撰寫額外程式碼,以避開副作用或撰寫額外程式碼,以清理磁帶,提供更直接的路徑,若要解決此問題需要一些經驗和信心。不過有時候您直覺是最佳指南。


Julie Lerman 是 Microsoft MVP,.NET 指導和居住在佛蒙特山區的顧問。 您可以找到其資料存取與其他.NET 主題,在使用者群組和世界各地的研討會上呈現。她的部落格網址 thedatafarm.com /blog 以及 Code First DbContext 版本中的,所有從 O'Reilly Media 是 「 程式設計 Entity framework 」。在 Twitter 上追隨她︰ @julielerman ,請參閱在她 Pluralsight 課程 juliel.me/PS 影片

由於閱本篇文章的下列技術專家︰ Andrea Saltarello (受管理的設計) (andrea.saltarello@manageddesigns.it)
Andrea Saltarello 是企業家及軟體架構設計人員米蘭,義大利,從仍都喜歡撰寫程式碼為真實的專案,以取得有關他的設計決策的意見反應。為培訓講師和喇叭,他已有課程和研討會的幾個讀出的合作跨歐洲 TechEd Europe、 DevWeek 等軟體架構設計人員。他自 2003年已是 Microsoft MVP,而且最近已受命 Microsoft 區域經理。他是熱衷於音樂,並用於第一次 Depeche 模式,與其他已經愛自從接聽 」 的所有項目計數 」。