下載代碼示例
這是我這一系列講座的最後一部分面向那些側重數據的開發者向他們講述域驅動的設計 (DDD) 所使用的一些更具挑戰性的編碼概念 作為使用 Entity Framework (EF) 的 Microsoft NET Framework 開發者並且在很長時間內從事數據優先(甚至數據庫優先)的開發工作我曾極其痛苦地試圖了解如何將我的技能與一些 DDD 實現技術相結合 即使我並沒有在項目中使用完整的 DDD 實現(從客戶端交互直到代碼)我仍從多種 DDD 工具中獲益匪淺
在最後一講中我將介紹 DDD 編碼的兩個重要技術模式以及如何將其應用到我所使用的對象關系映射 (ORM) 工具 EF 中 在之前的講座中我講述了一對一關系 在這裡我將論述 DDD 所側重的單向關系以及它們如何影響您的應用程序 這種選擇會導致困難的決策認識到您最好不使用 EF 執行的一些奇妙的關系 同時我還將討論一下在聚合根與存儲庫之間平衡任務的重要性
從根開始生成單向關系
從我開始使用 EF 生成模型時雙向關系已成為一種標准我不假思索地就使用這種關系 實現雙向導航的功能是有意義的 在擁有訂單和客戶的情況下能夠查看客戶的訂單是非常好的功能而且對於某個訂單能夠訪問其客戶數據也是非常便利的 無需多想我在訂單及其明細項目之間也生成了雙向關系 訂單與明細項目之間的關系確實會有用 但是如果您停下來稍微思考一下您擁有明細項目且需要追溯到其訂單的情況非常少見 我能想像到的這種情況之一是您在針對產品進行報告希望對通常哪些產品會一起訂購進行分析或者分析中涉及到客戶或發運數據 在這些情況下您可能需要從產品導航到包含該產品的明細項目然後回到訂單 不過我僅在報告場景中看到這種情況而這種情況下我不太需要處理側重於 DDD 的對象
如果我只需要從訂單導航到明細項目什麼方法可以最有效地描述我的模型中的此類關系?
如我所述DDD 側重於單向關系 Eric Evans 的建議是盡可能地限制關系非常重要以及了解域可能會發現自然的方向偏離管理復雜的關系特別是依賴於 Entity Framework 來維持關聯時絕對會導致許多混亂情況 我已經撰寫了大量關於數據點的專欄專門解釋了 Entity Framework 中的關聯 不論消除何種程度的復雜性都有可能會帶來好處
考慮一下我在這一系列中對於 DDD 使用過的簡單銷售模型在從訂單到其明細項目的方向中確實出現了偏差 我無法想像不從訂單開始創建刪除或編輯明細項目的情況
如果您回顧一下我以前在該系列中生成的 Order 聚合訂單並不控制明細項目 例如需要使用 Order 類的 CreateLineItem 方法來添加新的明細項目
public void CreateLineItem(Product product int quantity)
{
var item = new LineItem
{
OrderQty = quantity
ProductId = productProductId
UnitPrice = productListPrice
UnitPriceDiscount = CustomerDiscount + PromoDiscount
};
LineItemsAdd(item);
}
LineItem 類型具有 OrderId 屬性但沒有 Order 屬性 這意味著可以設置 OrderId 的值但不能從 LineItem 導航到實際的 Order 實例
在這種情況下按照 Evans 的話說施加了遍歷方向實際上我確保了能夠從 Order 遍歷到 LineItem但反方向則不行
這種方法有其含義不僅在模型中而且還在數據層內 我使用 Entity Framework 作為 ORM 工具它只需通過 Order 類的 LineItems 屬性便足以很好地理解此關系 由於我碰巧遵循了 EF 的約定它能夠理解 LineItemOrderId 是我的返回到 Order 類的外鍵屬性 如果我為 OrderId 使用了其他名稱對於 Entity Framework 來說這個過程就要復雜得多
但是在這一情形中我可以向現有訂單添加新的 LineItem如下所示
orderCreateLineItem(aProductInstance );
var repo = new SimpleOrderRepository();
repoAddAndUpdateLineItemsForExistingOrder(order);
repoSave();
order 變量現在表示帶有已有訂單和單個新 LineItem 的圖形 已有訂單來自數據庫並且 OrderId 中已經有值但新的 LineItem 只有 OrderId 屬性具有默認值該值為
我的存儲庫方法接受該訂單圖形將其添加到我的 EF 上下文中然後應用正確的狀態如圖 中所示
圖 將狀態應用到訂單圖形
public void AddAndUpdateLineItemsForExistingOrder(Order order)
{
_contextOrdersAdd(order);
_contextEntry(order)State = EntityStateUnchanged;
foreach (var item in orderLineItems)
{
// Existing items from database have an Id & are being modified not added
if (itemLineItemId > )
{
_contextEntry(item)State = EntityStateModified;
}
}
}
如果您不熟悉 EF 行為這裡加以說明Add 方法會導致上下文開始跟蹤圖形中的所有內容(訂單和單個明細項目) 同時使用 Added 狀態標記圖形中的每個對象 但是由於此方法側重於使用已有訂單我知道該 Order 不是新的因此該方法通過將 Order 實例設置為 Unchanged 來修復其狀態 它還檢查任何已有 LineItems 並將其狀態設置為 Modified從而在數據庫中更新它們而不是作為新項插入 在更為具體的應用程序中我傾向使用模式以更確定地了解每個對象的狀態不過在這個例子中我不希望過多涉及其他細節 (在 Rowan Miller 的博客上可以看到此模式的一個早期版本網址為 bitly/cLoo我們合著的書籍《Programming Entity Framework:DbContext》[OReilly Media ] 中提供了更新過的例子)
由於所有這些操作在上下文跟蹤對象時完成Entity Framework 還會神奇地在我的新 LineItem 實例中修復 OrderId 的值 因此在我調用 Save 時LineItem 知道了 OrderId 值為
不再使用神奇的 EF 關系管理 — 對於更新
出現這種好運氣是因為我的 LineItem 類型碰巧遵循了 EF 的外鍵名約定 如果我將它命名為 OrderId 之外的名稱例如 OrderFK則必須對類型進行一些更改(例如引入不需要的 Order 導航屬性)然後指定 EF 映射 這就不如人意了因為您增加了復雜性而只是為了滿足 ORM 有時候這種情況可能是必要的但如果不必要我希望能夠避免
更簡單的方法就是不再使用 EF 關系中奇妙的依賴關系而是控制代碼中外鍵的設置
第一步是告知 EF 忽略此關系否則它將繼續查找外鍵
下面是我在 DbContextOnModelBuilder 方法覆蓋中使用的代碼這樣 EF 就不會關注該關系
modelBuilderEntity<Order>()Ignore(o => oLineItems);
現在我將自行控制關系 這意味著重構因此我將構造函數添加到需要 OrderId 和其他值的 LineItem 中這使得 LineItem 更像是 DDD 實體我非常滿意 我還必須修改 Order 中的 CreateLineItem 方法以便使用該構造函數而不是對象初始值
圖 顯示了存儲庫方法的更新版本
圖 存儲庫方法
public void UpdateLineItemsForExistingOrder(Order order)
{
foreach (var item in orderLineItems)
{
if (itemLineItemId > )
{
_contextEntry(item)State = EntityStateModified;
}
else
{
_contextEntry(item)State = EntityStateAdded;
itemSetOrderIdentity(orderOrderId);
}
}
}
請注意我不用再添加訂單圖形然後將訂單的狀態修復為 Unchanged 實際上由於 EF 不了解關系如果我調用了 contextOrdersAdd(order)它會添加 order 實例但不會像以前一樣添加相關明細項目
相反我迭代圖形的明細項目不僅將現有明細項目的狀態設置為 Modified還將新明細項目的狀態設置為 Added 我使用的 DbContextEntry 語法完成兩項任務 在設置狀態之前它會檢查以了解上下文是否已意識到(或者跟蹤)該特定實體 如果沒有則它在內部連接實體 現在它可以響應代碼設置狀態屬性的情況 因此在該行代碼中我連接並設置 LineItem 的狀態
我的代碼現在也遵循將 EF 用於 DDD 的另一個忠告不要依賴於 EF 來管理關系 EF 執行許多奇妙的功能在許多情形下大有裨益 多年來我很高興地受益於其中 但是對於 DDD 聚合您實際上是希望在自己的模型中管理這些關系而不是依賴於數據層來為您執行必要的操作
由於我在為鍵(例如 OrderOrderId)使用整數並依賴於我的數據庫來為這些鍵提供值時陷入困境我需要在存儲庫中為新聚合(例如帶有明細項目的訂單)進行一些額外的工作 我需要緊密地控制持久性這樣才能使用舊式的插入圖形模式插入訂單獲取數據庫生成的新 OrderId 值將該值應用到新的明細項目然後將它們保存到數據庫 這是必需的因為我已經中斷了通常使用 EF 來完成這些奇妙操作的關系 您可以在下載的示例中查看我如何在存儲庫中實現這一點
經過了幾年我終於准備好停止依賴數據庫來創建我的標識符開始為我的鍵值使用可以在應用程序中生成和分配的 GUID 這使得我能夠進一步將我的域與數據庫分隔開
保持神奇的 EF 關系管理 — 對於查詢
在我的模型中放棄 EF 關系後對於在上一情形中執行更新確實非常有益 但是我並不希望放棄 EF 的所有關系功能 從數據庫查詢時加載相關數據是我希望留用的功能之一 不論是預先加載延遲加載還是顯式加載我樂於享受 EF 無需表示和執行附加查詢就能獲得相關數據的優點
這就是分離所關注概念的延伸觀點發揮作用的地方 在遵循 DDD 設計規則的過程中相似的類采用不同的表示形式很常見 例如您可能使用設計用於客戶管理上下文中的 Customer 類來完成此操作與之相對的是僅僅用於填充選取列表的 Customer 類該選取列表中只需要客戶的姓名和標識符
還可以采用不同的 DbContext 定義 在檢索數據的情形中您可能需要能意識到 Order 與 LineItems 之間關系的上下文這樣可以從數據庫中預先加載訂單及其明細項目 但是在執行我前面所進行的更新時您可能需要顯式忽略該關系的上下文這樣可以更加精確地控制域
對於您可能采用軟件解決的特定復雜問題子集這種情況的一個極端觀點是稱為命令查詢職責分離 (CQRS) 的模式 CQRS 引導您考慮將數據檢索(讀取)和數據存儲(寫入)視為單獨的系統需要不同的模型和體系結構 在一個小示例中我重點強調了對數據檢索操作和數據存儲操作采用不同的關系理解的優點這可以讓您了解 CQRS 所能幫助您實現的功能 您可以從 CQRS Journey 這個非常好的資源了解 CQRS 的更多信息網址為 msd/library/jj
數據訪問在存儲庫中進行而不是聚合根
現在我希望回顧一下並解決最後一個問題這個問題在我開始關注單向關系時困擾了我許久 (這並不是說再也沒有關於 DDD 的問題而是說這是我在這一系列中的最後一個主題)對於我們數據庫優先的思維方式這是一個關於單向關系的常見問題(使用 DDD)進行數據訪問的確切位置在哪裡?
EF 最初發布時唯一使用數據庫的方法是對現有數據庫實施反向工程 因此如前所述我習慣於每個關系都是雙向的 如果數據庫中的 Customers 和 Orders 表具有描述一對多關系的主鍵/外鍵約束那麼我在模型中就會看到這種一對多關系 客戶具有指向訂單集合的導航屬性 訂單具有指向 Customer 實例的導航屬性
在發展到模型優先和代碼優先(可以描述模型和生成數據庫)的過程中我繼續使用該模式在關系的兩端定義導航屬性 EF 很好用映射更簡單編碼也更自然
因此在 DDD 中當我發現使用的 Order 聚合根能夠意識到 CustomerId 甚至可能意識到完整的 Customer 類型但是卻無法從 Order 導航回 Customer 時我非常沮喪 我首先提出的問題是如果我要查找某個客戶的所有訂單應該怎麼辦?我始終認為我應該能夠這麼做而且我習慣於依賴具有雙向導航的訪問
如果邏輯是從我的訂單聚合根開始我該怎麼解決這個問題? 最初我也錯誤地認為要通過聚合根來完成所有操作但這無濟於事
實際的解決方案讓我覺得自己愚不可及 在這裡我分享了自己的愚蠢想法以防有人跟我一樣誤入歧途 能夠幫助我解決問題的既不是聚合根也不是 Order 不過在側重於 Order 的存儲庫中(也是我用於執行查詢和持久性的存儲庫)我的問題的答案顯而易見
public List<Order>GetOrdersForCustomer(Customer customer)
{
return _contextOrders
Where(o => oCustomerId == customerId)
ToList();
}
該方法返回 Order 聚合根的列表 當然如果我在 DDD 的工作范圍中創建此項並且我知道必須在特定上下文中使用它而不是以防萬一那麼我會不辭辛苦地將該方法放到存儲庫中有可能我會在報告應用程序或者類似應用程序中需要它但是在針對生成銷售訂單設計的上下文中則無必要
我的問題剛剛開始
在過去的幾年中我對 DDD 有所了解此系列中我介紹的主題是我在數據層中使用 Entity Framework 時在理解或弄清如何實現的過程中遇到的最困難的問題 其中一些挫折源自我多年來思考軟件的角度我習慣於從我的數據庫工作方式來考慮問題 轉變了這個角度之後豁然開朗因為這讓我將重點放在眼前的問題上也就是我所設計軟件的域問題 與此同時我確實需要找到良好的平衡因為在添加到我的解決方案中時可能會出現數據層問題
在使用 Entity Framework 將我的類直接映射回數據庫時我重點思考的是其工作原理不過考慮到域邏輯與數據庫之間可能存在另一層(或更多層)也很重要 例如您可能會有一個域邏輯與之交互的服務 在這種情況下從您的域邏輯中映射時數據層的重要性很低(或者根本不重要)這種問題現在由服務來處理
有很多方法可用於實現軟件解決方案 即使我沒有實現完整的端到端 DDD 方法(這需要對此相當得精通)我的整個工作仍然從通過 DDD 學習到的知識和技術中獲益頗多
From:http://tw.wingwit.com/Article/program/Web/201404/30636.html