国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
面向領域驅動架構的查詢實現方式
面向領域驅動架構的查詢實現方式
作者: dax.net   來源: 博客園   發(fā)布時間: 2012-01-20 13:43  閱讀: 1461 次  推薦: 0   原文鏈接    [收藏]   

在上一篇文章《.NET應用框架架構設計實踐 - 概述》的評論部分,有網友提出了一個在面向領域驅動架構的實踐中比較常見的問題:“DDD使用聚合根訪問,那例如那些通用查詢如何實現?難道都要經過聚合根多步得到么?DDD如何實現關聯表的查詢,例如3表關聯查詢?”這個問題比較泛,涉及的內容也比較多,我就單獨一篇文章介紹一下我對這個問題的看法。關于上面問題中的“通用查詢”- 呃,這個定義比較模糊,我只能給出我的一些想法或者經驗性的東西,我在本文中的經驗與觀點并不一定會100%適合您的應用場景,但我想應該還是具有一定指導性意義的。

  聚合與聚合根

我想,還是從聚合根談起吧。聚合根是DDD中的概念,不管是經典的DDD架構,還是基于事件驅動的CQRS架構,其實它們之間絕大部分概念都是相通的,比如實體、值對象、服務、工廠、倉儲以及聚合/聚合根等。根據我的理解,聚合根是一個實體,它保持著與其它實體/值對象的引用,并與這些實體/值對象一起,來表達領域的通用語言中的一個唯一的無二義的邏輯概念。比如最常見的“客戶(Customer)”,在“在線銷售”的領域中,“客戶”不僅包含它所指代的那個個人(或者是組織)的名稱、聯系電話、聯系電郵,還會包含它的聯系地址(Contact Address)以及送貨地址(Delivery Address),那么就Address而言,在此我們可以將其視為值對象,因為我們只關心地址本身所包含的信息。在這里,“客戶(Customer)”不僅是實體,而且是“客戶-地址”所組成的對象集合(聚合)的聚合根。

在這里會有異議的地方就是“銷售訂單(Sales Order)”是否應該屬于“客戶(Customer)”聚合。我覺得這還是要看在當前的領域中,“銷售訂單”是不是“客戶”的必有信息,換句話說,“客戶”是不是沒有“銷售訂單”就不成其為“客戶”。我想,在大多數情況下,“客戶”應該是一個可以脫離“銷售訂單”而單獨存在的實體,那這樣的話,“銷售訂單”也將不屬于“客戶”聚合。

現在讓我們來看“在線銷售”領域中的另一部分:銷售訂單。當然,“銷售訂單(Sales Order)”是實體,本身也是訂單主體與“訂單明細(Sales Lines)”所組成的聚合的聚合根,這是很自然的事情,因為“銷售訂單”如果沒有訂單的明細信息,也就失去了訂單本身的意義。此外,“客戶”實體也是這個聚合的一個組成部分,這也很好理解,“銷售訂單”本身就是客戶下達的,它不可能脫離“客戶”而憑空存在。于是,以“銷售訂單”為根的聚合,還包括“客戶”實體,以及“訂單明細”(至于“訂單明細”是實體還是值對象,這跟具體的領域定義有密切關系,比如如果涉及商品Item與購買量的打折等內容,那么“訂單明細”就需要以實體方式處理,否則可以設計成“值對象”以減小系統開銷,本文繞過這個問題的討論)。在作進一步討論之前,讓我們回顧一下DDD中的倉儲。DDD告訴我們,倉儲是作用在聚合根上的:領域模型中對象的保存與讀取都是以聚合為單位而進行的。

通過上面的討論,針對“在線銷售”領域,我們大致得到了如下的領域模型(為了縮短篇幅,圖中可能會省略某些部分)

問題來了,如果我們需要獲得某個“客戶”的所有訂單,該怎么辦?在上面的領域模型中,Customer實體并沒有某個屬性或者方法來獲得其所有的銷售訂單。那么在遇到這樣的問題時,通常都是通過SalesOrder的倉儲,配合規(guī)約(Specification)來篩選出所有符合特定“客戶”條件的銷售訂單,然后由倉儲返回銷售訂單的列表。你或許會覺得這種做法比較不科學,你會覺得應該通過Customer實體的某個屬性(比如SalesOrders)來獲得該“客戶”所擁有的所有銷售訂單,這樣會更直截了當些。但在上面我們已經對這個領域模型進行了討論,在我們的案例中,Customer是一個獨立的實體,SalesOrder不是它的必要組成部分。于是,為了維護領域模型的完整性,我們需要利用“銷售訂單”的倉儲來完成這個功能。偽代碼如下:

public interface ISpecification<T>
{
bool IsSatisfiedBy(T obj);
}
public abstract class Specification<T> : ISpecification<T>
{
public abstract Expression<Func<T, bool>> Expression { get; }
public bool IsSatisfiedBy(T obj)
{
return this.Expression.Compile()(obj);
}
}
public class OrderCustomerMatchesSpecification : Specification<SalesOrder>
{
private Customer customer;
public OrderCustomerMatchesSpecification(Customer customer)
{
this.customer = customer;
}
public override Expression<Func<SalesOrder, bool>> Expression
{
get { return p => p.Customer.Id.Equals(customer.Id); }
}
}
public interface IRepository<T>
where T : IAggregateRoot
{
void Add(T aggregateRoot);
List
<T> GetAllBySpecification(ISpecification<T> spec);
}
public class MemoryRepository<T> : IRepository<T>
where T : IAggregateRoot
{
private readonly List<T> store =new List<T>();
public void Add(T aggregateRoot)
{
if (!this.store.Exists(p => p.Id.Equals(aggregateRoot.Id)))
this.store.Add(aggregateRoot);
}
public List<T> GetAllBySpecification(ISpecification<T> spec)
{
return this.store.Where(spec.IsSatisfiedBy).ToList();
}
}
ISpecification
<SalesOrder> spec =new OrderCustomerMatchesSpecification(custDaxnet);
List
<SalesOrder> daxnetOrders = salesOrderRepository.GetAllBySpecification(spec);

在上面的代碼中,daxnetOrders對象所保存的就是所有屬于custDaxnet這個Customer的銷售訂單。通過這個例子我們可以看出,當我們需要某些信息的時候,我們只與領域模型中的聚合、實體、值對象以及倉儲打交道,我們完全沒有涉及任何數據庫、數據表、字段、記錄等等這些概念,從上面的代碼也可以看出,我們可以使用服務樁Service Stub,PoEAA)模式來Mock一個基于內存的倉儲,與關系型數據庫毫不相干。事實上也是如此,我們軟件設計者、開發(fā)者以及領域專家在同一個事物上達成共識:領域模型。聚合、實體、值對象等成為領域模型的主要組成部分,而這些對象又各自保持著自己的狀態(tài),也就是我們所需要的數據。在經典的DDD架構風格(例如Microsoft NLayerApp這樣的架構)中,我們通過領域模型中的對象及其之間的關系來獲得我們所需要的信息,因此,數據的查詢應該是由倉儲引起,并通過聚合實現導航(Navigation)查詢。接下來,讓我們引入關系型數據庫,來談談本文最開始提出的“多個表關聯查詢”的問題。

  領域模型 vs 關系型數據庫

在我之前所寫的《經典的應用系統結構、CQRS與事件溯源》一文中,討論了領域模型與關系型數據模型之間的“阻抗失衡”效應,在此也就不再重復了,但我們必須弄清楚一件事情,就是在DDD的實踐中,我們必須拋開關系型數據庫,甚至是其它的一切數據持久化機制,而只關注領域模型。于是,領域模型本身也需要屏蔽數據持久化的細節(jié)內容(我們通常稱之為“持久化無關性”,Persistence Ignorance)。這有兩個方面的原因:首先,DDD是面向領域的,不是面向數據的,領域模型對問題域進行了表述,這也是軟件人員與領域專家的溝通橋梁,如果引入數據存儲的細節(jié)內容,既不利于溝通,也會使得領域模型過多依賴具體的技術實現方案,提高了系統的耦合度;其次,由于“阻抗失衡”效應的存在,就需要有一個中介角色來解決這個失衡效應,通常是ORM承擔了這個角色,然而,從技術實現的角度看,針對同一個領域模型,ORM可以有不同的處理方式,具體采用哪種處理方式,可以通過ORM框架的配置信息(例如,NHibernate的hbm映射文件)來決定;在這種情況下,領域模型+ORM決定了關系型數據庫的結構,于是,對數據表、字段、記錄等關系型數據庫的討論就沒多大意義了,因為關系型數據庫本身的結構也是不確定的?,F在,讓我們來看個例子,了解一下ORM處理同一個領域模型的不同方式。就以上文所提到的“客戶 - 地址”聚合為例,ORM處理這個聚合至少(但不限于)可以有如下四個方式:

  • 外鍵映射模式(Foreign Key Mapping Pattern,PoEAA)
    這種方式會將對象間的關系映射到數據表的外鍵關聯。比如“客戶 - 地址”聚合,ORM會在數據庫中產生兩張表:Customer表和Address表,Customer表中包含兩個Address記錄的外鍵引用:

  • 關聯表映射模式(Association Table Mapping Pattern,PoEAA)
    這種方式會引入第三張數據表,用來保存另外兩張表之間的主鍵關聯。比如“客戶 - 地址”聚合,ORM會在數據庫中產生三張表:Customer表、Address表以及CustomerAddress表:

  • 嵌入值模式(Embedded Value Pattern,PoEAA)
    嵌入值模式會將一個對象映射成另一個對象表的若干字段。比如“客戶 - 地址”聚合,ORM僅會在數據庫中產生一張表:Customer表,其中包含了Address對象所有屬性值的字段:

  • 序列化LOB模式(Serialized LOB Pattern,PoEAA)
    該模式會將另一對象的數據序列化成一個LOB(BLOB或者CLOB),然后以一個字段的形式保存在當前對象所對應的數據表中。比如“客戶 - 地址”聚合,ORM會在數據庫中產生一張數據表:Customer表,并在其中保存“地址”對象的序列化LOB數據:

因此,在DDD實踐中,我們不會存在“如何進行關聯表查詢”這樣的問題,我們關注的是領域模型,至于關系型數據庫方面的工作,就交給ORM吧。

當然,理論歸理論,實際項目與理論上的東西相差太大,我們也需要具體問題具體分析。例如,ORM的引入雖然解決了領域模型與關系型數據模型之間的“阻抗失衡”,但也帶來了一定程度的性能問題,對于某些性能要求很高的系統,采用DDD實踐可能就不是一個很好的選擇,當然也可以想辦法找一個折中的方式來處理問題。比如,假設某個系統基本上對性能要求不高,可以采用DDD的實踐方式,只是個別查詢功能(比如總賬報表生成、數據統計等)要求高效,此時,我們還是可以應用DDD的實踐經驗,并試圖在這幾個功能上繞過領域模型,直接采用高效率的數據庫查詢方式(比如ADO.NET),當然這已經脫離了DDD的討論范圍,不過我們的目的就是為了實現一套穩(wěn)定、安全、高效的系統,DDD或不DDD這并不是重點,重點在于合適就好。我想,這也是架構師的職責所在吧。

在我們采用“非正常手段”慢慢地繞過領域模型的時候,我們會發(fā)現一個有趣的現象:其實“查詢”根本就不是領域模型的一部分,“查詢”是可以作為一個單獨的系統而獨立存在的,在需要的時候,這個“查詢系統”可以被整合到實際系統當中(比如采用Microsoft Biztalk Server等手段),為客戶端提供查詢服務。既然“查詢”可以是一個單獨的系統,那么如何實現這個“查詢”系統,方法也就五花八門了:可以繼續(xù)結合ORM實現查詢,也可以直接寫SQL語句進行查詢,甚至還可以使用一些現有的查詢框架,總之只要能夠向客戶端提供所需要的數據就行了。“查詢”不再受到領域模型的牽制,在如此廣泛的技術選型背景下,我想,要實現一套復雜的、可定制的查詢機制根本就不會是什么難事。

面向領域驅動的CQRS(Command Query Responsibility Segregation,命令查詢職責分離)架構就是這樣一種架構風格:它完全將“查詢”部分從領域模型中分離出來。

  CQRS體系結構模式

在我之前所寫的《EntityFramework之領域驅動設計實踐【擴展閱讀】:CQRS體系結構模式》一文(以下簡稱《CQRS》)中,已經非常詳細地對CQRS體系結構模式進行了介紹和總結,在這里再對這種結構的“查詢”部分簡要地說幾句。

在CQRS中,我們可以看到,作用在聚合根上的“倉儲”,已經退化成“領域倉儲(Domain Repository)”,領域倉儲也是作用在聚合根上的,但它只有兩個操作:Save以及GetByAggregateRootId。顯而易見,Save的功能就是將整個聚合保存起來,而GetByAggregateRootId則是通過聚合根的標識來獲得整個聚合。于是,像上面我所例舉的“獲取某個客戶的所有銷售訂單”這樣的操作,在CQRS的Command部分是無法完成的:你無法通過規(guī)約(Specification)來獲得“包含”某個客戶的所有訂單,你只能夠通過訂單號來獲取訂單信息?;蛟S(我是說或許),在CQRS架構的領域模型中我們根本無需知道某個訂單是屬于哪個客戶的,OK,直接將“客戶”實體從“銷售訂單”聚合中排除出去。關于這個問題我在領域驅動設計的官方論壇里討論過,得到的結論就是:領域模型只應該包含必要的信息,一切與查詢有關的內容,都應該設計在“查詢”部分。

在《CQRS》一文中我已經給出了一張結構圖,現在我再細化一下這個圖以體現其查詢部分的具體情況:

在上圖中,領域模型在完成操作之后,會產生領域事件,在聚合被保存到數據庫的同時,領域事件也會被發(fā)布到事件總線(Event Bus)上。然后,事件派發(fā)處理器(Event Dispatcher,在這里使用的是Microsoft Biztalk Server)會將事件派發(fā)到各種不同的訂閱機制,比如Dynamics AX系統或者單獨的查詢數據庫。這樣,查詢數據庫將會有較大的設計空間(比如可以根據客戶端View Model來設計關系型數據庫的表結構),Query Reader的設計也會變得非常簡單。在這樣的結構下,實現通用查詢、復雜查詢也會非常簡單。

  總結

總之,領域模型可以提供一定的查詢能力,比如通過倉儲、規(guī)約以及對象關系導航等方式獲得所需要的數據,但查詢應該不是領域模型的組成部分,它是可以被分離出去的。對于經典的架構風格(比如Microsoft NLayerApp這樣的架構風格),如果需要獲得復雜的查詢功能,那就直接繞過領域模型,單獨出一個系統直接訪問數據庫進行查詢,然后把查詢返回給客戶端;客戶端獲得查詢結果后,再根據修改過的數據,通過倉儲獲得領域對象然后更新領域模型;對于CQRS的架構風格,我們將獲得更大的查詢部分的設計空間,查詢功能的實現也不再成為問題。

希望本文能夠對關注這方面內容的讀者朋友一定的幫助。

本站僅提供存儲服務,所有內容均由用戶發(fā)布,如發(fā)現有害或侵權內容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
領域驅動設計實踐 —— 架構風格及架構實例
淺析DDD(領域驅動設計)
EntityFramework之領域驅動設計實踐【擴展閱讀】:CQRS體系結構模式 - d...
DDD --領域驅動設計
使用函數式語言實踐DDD
基于Spring Boot實現DDD貨物運輸微服務應用源碼
更多類似文章 >>
生活服務
分享 收藏 導長圖 關注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點擊這里聯系客服!

聯系客服