DDD理论学习系列(12)-- 仓储
1. 引言
DDD中Repository這個單詞,主要有兩種翻譯:資源庫和倉儲,本文取倉儲之譯。
說到倉儲,我們肯定就想到了倉庫,倉庫一般用來存放貨物,而倉庫一般由倉庫管理員來管理。當(dāng)工廠生產(chǎn)了一批貨物時,只需交給倉庫管理員即可,他負責(zé)貨物的堆放;當(dāng)需要發(fā)貨的時候,倉庫管理員負責(zé)從倉庫中撿貨進行貨物出庫處理。當(dāng)需要庫存盤點時,倉庫管理員負責(zé)核實貨物狀態(tài)和庫存。換句話說,倉庫管理員負責(zé)了貨物的出入庫管理。通過倉庫管理員這個角色,保證了倉庫和工廠的獨立性,工廠只需要負責(zé)生產(chǎn)即可,而至于貨物如何存放工廠無需關(guān)注。
而我們要講的倉儲就類似于倉庫管理員,只不過它負責(zé)的不再是貨物的管理,而是聚合的管理,倉儲介于領(lǐng)域模型和數(shù)據(jù)模型之間,主要用于聚合的持久化和檢索。它隔離了領(lǐng)域模型和數(shù)據(jù)模型,以便我們關(guān)注于領(lǐng)域模型而不需要考慮如何進行持久化。
2. DDD中的倉儲
2.1. 倉儲的集合特性
倉儲代表一個聚合的集合,其行為與.Net集合一樣,倉儲用來存儲和刪除聚合,但同時提供針對聚合的顯式查詢以及匯總。
2.2. 倉儲與數(shù)據(jù)訪問層的區(qū)別
倉儲限定了只能通過聚合根來持久化和檢索領(lǐng)域?qū)ο?#xff0c;以確保所有改動和不變性由聚合處理。
倉儲通過隱藏聚合持久化和檢索的底層技術(shù)實現(xiàn)領(lǐng)域?qū)拥牡某志没療o關(guān)性(即領(lǐng)域?qū)硬恍枰廊绾纬志没I(lǐng)域?qū)ο?#xff09;。
倉儲在數(shù)據(jù)模型和領(lǐng)域模型定義了一個邊界。
2.3. 倉儲舉例
下面我們首先來看一個簡單倉儲的定義:
namespace DomainModel{public interface ICustomerRepository{
Customer FindBy(Guid id);
void Add(Customer customer); void Remove(Customer customer);} }
通常來說,倉儲由應(yīng)用服務(wù)層調(diào)用。倉儲定義應(yīng)用服務(wù)執(zhí)行業(yè)務(wù)用例時需要的所有的數(shù)據(jù)訪問方法。而倉儲的實現(xiàn)通常位于基礎(chǔ)架構(gòu)層,由持久化框架來支撐。以下的倉儲實現(xiàn)是借助于ORM框架Nhibernate的ISession接口,它扮演一個的網(wǎng)關(guān)角色,負責(zé)領(lǐng)域模型和數(shù)據(jù)模型的映射。
namespace Infrastructure.Persistence { ??public class CustomerRepository : ICustomerRepository { ? ? ?
?
? ?private ISession _session; ?
? ?? ? ?
? ?? ? ?public CustomerRepository (ISession session) {_session = session;} ? ?
? ?
? ? ? ?public IEnumerable<Customer> FindBy (Guid id) ? ? ? ? ? ?return _session.Load<Order> (id);} ? ? ?
? ? ?
? ? ? ?public void Add (Customer customer) {_session.Save (customer);} ? ? ? ?public void Remove (Customer customer) {_session.Delete (customer);}} }
從上面我們可以看出,將領(lǐng)域模型的持久化轉(zhuǎn)移到基礎(chǔ)設(shè)施層,隱藏了領(lǐng)域模型的技術(shù)復(fù)雜性,從而使領(lǐng)域?qū)ο竽軌驅(qū)W⒂跇I(yè)務(wù)概念和邏輯。
2.4. 倉儲的誤解
倉儲也存在很多誤解,許多人認為其是不必要的抽象。當(dāng)應(yīng)用于簡單的領(lǐng)域模型時,可以直接使用持久化框架來進行數(shù)據(jù)訪問。然而當(dāng)對復(fù)雜的領(lǐng)域模型進行建模時,倉儲是模型的擴展,它表明聚合檢索的意圖,可以對領(lǐng)域模型進行有意義的讀寫,而不是一個技術(shù)框架。
也有很多人認為倉儲是一種反模式,因為其隱藏了基礎(chǔ)持久化框架的功能。而恰巧這正是倉儲的要點。基礎(chǔ)持久化框架提供了開放的接口用于對數(shù)據(jù)模型的查找和修改,而倉儲通過使用定義的命名查詢方法來限制對聚合的訪問。通過使查詢顯式化,就更容易調(diào)整查詢,且更重要的是倉儲明確了查詢的意圖,便于領(lǐng)域?qū)<依斫狻Ee個例子:我們在倉儲中定義了一個方法GetAllActiveUsers()與sql語句select * from users where isactive = 1或var users =db.Users.Where(u=>u.IsActive ==1)相比,很明顯倉儲的方法命名就能讓我們明白了查詢的意圖:查詢所有處于Active狀態(tài)的用戶。除了查詢,倉儲僅暴露必要的持久化方法而不是提供所有的CURD方法。
2.5. 倉儲的要點
倉儲的要點并不是使代碼更容易測試,也不是為了便于切換底層的持久化存儲方式。當(dāng)然,在某種程度上,這也的確是倉儲所帶來的利好。倉儲的要點是保持你的領(lǐng)域模型和技術(shù)持久化框架的獨立性,這樣你的領(lǐng)域模型可以隔離來自底層持久化技術(shù)的影響。如果沒有倉儲這一層,你的持久化基礎(chǔ)設(shè)施可能會泄露到領(lǐng)域模型中,并影響領(lǐng)域模型完整性和最終一致性。
3. 領(lǐng)域模型 VS 數(shù)據(jù)模型
如果選擇關(guān)系型數(shù)據(jù)庫作為持久化存儲,我們可以借助于ORM框架來實現(xiàn)領(lǐng)域模型和數(shù)據(jù)模型之間的映射和持久化操作。
而ORM又是什么呢?
按照文章開頭中的例子,如果倉儲對應(yīng)倉庫管理員的角色,那ORM就相當(dāng)于倉庫機器人,而倉庫就相當(dāng)于數(shù)據(jù)庫。為了方便不同商品的歸類存放,對倉庫進行分區(qū),分區(qū)就相當(dāng)于數(shù)據(jù)表。當(dāng)公司接到一筆訂單做發(fā)貨處理時,銷售員將發(fā)貨通知單告知倉庫管理員,倉庫管理員再分配ORM機器人進行撿貨。很顯然,ORM機器人必須能夠識別發(fā)貨通知單,將發(fā)貨通知單中的商品對應(yīng)到倉庫中存儲的貨物。這里面發(fā)貨通知單就相當(dāng)于領(lǐng)域模型,而倉庫中存儲的貨物就屬于數(shù)據(jù)模型。
相信基于上面的比喻,我們對ORM有了基本的認識。ORM,全稱是Object Relational Mapping,對象關(guān)系映射。ORM的前提是,將對象的屬性映射到數(shù)據(jù)庫字段,將對象之間的引用映射到數(shù)據(jù)庫表的關(guān)系。換句話說,ORM負責(zé)將代碼中定義的對象和關(guān)系映射到數(shù)據(jù)庫的表結(jié)構(gòu)中去,并在進行數(shù)據(jù)訪問時再將表數(shù)據(jù)映射到代碼中定義的對象,借助ORM我們不需要去手動寫SQL語句就可以完成數(shù)據(jù)的增刪改查。ORM僅僅抽象了關(guān)系數(shù)據(jù)模型,它只是以面向?qū)ο蟮姆绞絹肀硎緮?shù)據(jù)模型,以方便我們在代碼中輕松地處理數(shù)據(jù)。
下面我們來探討一下數(shù)據(jù)模型與領(lǐng)域模型的異同。關(guān)系數(shù)據(jù)庫中的數(shù)據(jù)模型,它由表和列組成,它只是簡單的存儲結(jié)構(gòu),用于保存領(lǐng)域模型某個時間點的狀態(tài)。數(shù)據(jù)模型可以分散在幾個表甚至幾個數(shù)據(jù)庫中。此外,可以使用多種形式的持久化存儲,例如文件、web服務(wù)器、關(guān)系數(shù)據(jù)庫或NoSQL。領(lǐng)域模型是對問題域的抽象,具有豐富的語言和行為,由實體和值對象組成。對于一些領(lǐng)域模型,可能與數(shù)據(jù)模型相似,甚至相同,但在概念上它們是非常不同的。ORM與領(lǐng)域模型無關(guān)。倉儲的作用就是將領(lǐng)域模型與數(shù)據(jù)模型分開,而不是讓它們模糊成一個模型。ORM不是倉儲,但是倉儲可以使用ORM來持久化領(lǐng)域?qū)ο蟮臓顟B(tài)。
如果你的領(lǐng)域模型與你的數(shù)據(jù)模型類似,ORM可以直接映射領(lǐng)域模型到數(shù)據(jù)存儲,否則,則需要對ORM進行額外的映射配置。
4. 倉儲的定義和實現(xiàn)
上面也提到過,我們一般在領(lǐng)域?qū)佣x倉儲接口,在基礎(chǔ)設(shè)施層實現(xiàn)倉儲,以隔離領(lǐng)域模型和數(shù)據(jù)模型。
4.1. 倉儲方法需明確
倉儲是原則上是領(lǐng)域模型與持久化存儲之間明確的契約,倉儲定義的接口方法不僅僅是CURD方法。它是領(lǐng)域模型的擴展,并以領(lǐng)域?qū)<宜斫獾男g(shù)語編寫。倉儲接口的定義應(yīng)該根據(jù)應(yīng)用程序的用例需求來創(chuàng)建,而不是從類似CURD的數(shù)據(jù)訪問角度來構(gòu)建。
我們來看一段代碼:
namespace DomainModel { ??public interface ICustomerRepository { ? ? ?
? ? ??Customer FindBy (Guid id); ? ?
??? ??IEnumerable<Customer> FindAllThatMatch (Query query); ? ? ?
??? ??IEnumerable<Customer> FindAllThatMatch (String hql); ?
??? ? void Add (Customer customer);} }
以上倉儲定義了一個FindAllThatMatch方法以支持客戶端以任何方式查詢領(lǐng)域?qū)ο蟆_@個方法的設(shè)計思想無可置否,靈活且可以擴展,但是它并沒有明確的表明查詢的意圖,我們就失去了對查詢的控制。為了真正了解如何使用這些方法,開發(fā)人員需要跟蹤相關(guān)調(diào)用堆棧,才能知悉方法的意圖,更別說出現(xiàn)性能問題時如何著手優(yōu)化了。因為倉儲定義的接口方法過于寬泛且不具體,它模糊了領(lǐng)域的的概念,所以定義這樣的一個接口方法是無意義的。
我們可以如下改造:
namespace DomainModel { ??public interface ICustomerRepository { ? ? ?
?
??Customer FindBy (Guid id); ? ? ?
?? ?IEnumerable<Customer> FindAllThatAreDeactivated (); ? ?
?? ?IEnumerable<Customer> FindAllThatAreOverAllowedCredit (); ?
?? ?void Add (Customer customer);} }
通過以上改造,我們通過方法的命名來明確查詢的意圖,符合通用語言的規(guī)范。
4.2. 泛型倉儲
在實踐中我們可能會發(fā)現(xiàn),為每一個聚合定義一個倉儲會導(dǎo)致重復(fù)代碼,因為大部分的數(shù)據(jù)操作都是類似的。為了代碼重用,泛型倉儲就應(yīng)時而生。
泛型倉儲舉例:
namespace DomainModel { ??public interface IRepository<T> where T : EntityBase { ? ?
? ? ?T GetById (int id);IEnumerable<T> List ();IEnumerable<T> List (Expression<Func<T, bool>> predicate); ?
? ? ?? ?void Add (T entity); ?
? ? ? ? void Delete (T entity); ? ?
? ? ? ? void Edit (T entity);} ?
? ? ? ? ?public abstract class EntityBase { ? ? ?
? ? ? ? ?
? ? ? ? ? ?public int Id { get; protected set; }} }
泛型倉儲實現(xiàn):
namespace Infrastructure.Persistence {? ?public class Repository<T> : IRepository<T> where T : EntityBase { ? ? ?
? ??private readonly ApplicationDbContext _dbContext; ? ?
? ? ? ?public Repository (ApplicationDbContext dbContext) {_dbContext = dbContext;} ? ? ?
? ? ? ?
? ? ? ? ?public virtual T GetById (int id) { ? ? ?
? ? ? ? ? ? ? ?return _dbContext.Set<T> ().Find (id);} ?
? ? ? ? ? ? ? ?
? ?? ? ?public virtual IEnumerable<T> List () { ? ? ? ?
? ?? ? ?
? ?? ? ? ?return _dbContext.Set<T> ().AsEnumerable ();} ? ? ?
? ?? ? ?
? ?? ? ?public virtual IEnumerable<T> List (Expression<Func<T, bool>> predicate) { ? ? ?
? ?? ? ?? ? ?return _dbContext.Set<T> ().Where (predicate).AsEnumerable ();} ? ?
? ??? ?public void Insert (T entity) {_dbContext.Set<T> ().Add (entity);_dbContext.SaveChanges ();} ? ? ?
? ??? ?
? ??? ??public void Update (T entity) {_dbContext.Entry (entity).State = EntityState.Modified;_dbContext.SaveChanges ();} ? ?
? ??? ??
? ? ? ?public void Delete (T entity) {_dbContext.Set<T> ().Remove (entity);_dbContext.SaveChanges ();}} }
通過定義泛型倉儲和默認的實現(xiàn),很大程度上進行了代碼重用。但是,嘗試將泛型倉儲應(yīng)用所有倉儲并不是一個好的主意。對于簡單的聚合我們可以直接使用泛型倉儲來簡化代碼。但對于復(fù)雜的聚合,泛型倉儲可能就會不太適合,如果基于泛型倉儲的方法進行數(shù)據(jù)訪問,就會模糊對聚合的訪問意圖。
對于復(fù)雜的聚合,我們可以重新定義:
namespace DomainModel { ??public interface ICustomerRepository { ? ? ?
?
? ? ? ??Customer FindBy (Guid id); ? ? ? ?
??
?? ? IEnumerable<Customer> FindAllThatAreDeactivated (); ? ?
?? ? ?void Add (Customer customer);} }
在實現(xiàn)時,我們可以引用泛型倉儲來避免代碼重復(fù)。
namespace Infrastructure.Persistence { ??public class CustomerRepository : ICustomerRepository { ? ?
? ? ?private IRepository<Customer> _customersRepository; ?
? ? ?
? ? ? ? ? ?public Customers (IRepository<Customer> customersRepository) {_customersRepository = customersRepository;} ? ? ? ?// ....
? ? ? ? ? ?public IEnumerable<Customer> FindAllThatAreDeactivated () {_customersRepository.List(c => c.IsActive == false);} ? ? ?
? ? ? ? ? ??public void Add (Customer customer) {_customersRepository.Add (customer);}} }
通過這種方式,我們即明確了查詢了意圖,又簡化了代碼。
4.3. IQueryable Vs IEnumerable
在定義倉儲方法的返回值時,我們可能會比較疑惑,是應(yīng)該直接返回數(shù)據(jù)(IEnumerable)還是返回查詢(IQueryable)以便進行進一步的細化查詢?返回IEnumerable會比較安全,但IQueryable提供了更好的靈活性。事實上,如果使用IQueryable作為返回值,我們僅提供一種讀取數(shù)據(jù)的方法即可進行各種查詢。
但是這種方式就會引入一個問題,就是業(yè)務(wù)邏輯會滲透到應(yīng)用層中去,并出現(xiàn)大量重復(fù)。比如,在實體中我們一般使用IsActive或IsDeleted屬性來表示軟刪除,而一旦實體中的某條數(shù)據(jù)被刪除,那么UI中基本不會再顯示這條數(shù)據(jù),那對于實體的查詢都需要包含類似Where(c=> c.IsActive)的linq表達式。對于這種問題,我們最好在倉儲中的方法中,比如List()或者ListActive()做默認處理,而不是在應(yīng)用服務(wù)層每次去指定查詢條件。
但具體是返回 IQueryable還是IEnumerable每個人的看法不一,具體可參考Repository 返回 IQueryable?還是 IEnumerable?。
5. 事務(wù)管理和工作單元
事物管理主要是應(yīng)用服務(wù)層的關(guān)注點。然而,因為倉儲和事物管理緊密相關(guān)的。倉儲僅關(guān)注單一聚合的管理,而一個業(yè)務(wù)用例可能會涉及到多種的聚合。
事物管理由UOW(Unit of Work)處理。UOW模式的作用是在業(yè)務(wù)用例的操作中跟蹤聚合的所有更改。一旦發(fā)生了更改,UOW就使用事務(wù)來協(xié)調(diào)持久化存儲。為了確保數(shù)據(jù)的完整性,如果提交數(shù)據(jù)失敗,則會回滾所有更改,以確保數(shù)據(jù)保持有效狀態(tài)。
而關(guān)于UOW又是一個復(fù)雜的話題,我們后續(xù)再講。
6. 倉儲的反模式(注意事項)
不要支持臨時查詢(ad hoc query)
倉儲不應(yīng)該開放擴展,不要為了支持多種形式的查詢,定義比較寬泛的查詢方法,它不僅不能明確表達倉儲查詢的意圖,更可能會導(dǎo)致查詢性能。
延遲加載是一種設(shè)計臭味
聚合應(yīng)圍繞不變性構(gòu)建,并包含所有必需的屬性去支持不變性。 因此,當(dāng)加載聚合時,要么加載所有,要么一個也不加載。 如果您有一個關(guān)系數(shù)據(jù)庫并且正在使用ORM作為數(shù)據(jù)模型,那么您可能能夠延遲加載一些領(lǐng)域?qū)ο髮傩?#xff0c;這樣就可以推遲加載不需要的聚合部分。但是,這樣做的問題是,如果您只能部分加載聚合,可能會導(dǎo)致您的聚合邊界錯誤。
不要使用聚合來實現(xiàn)報表需求
報表可能會涉及到多個類型的聚合,而倉儲是處理單一聚合的。另外倉儲是基于事務(wù)的,可能會導(dǎo)致報表的性能問題。
7. 總結(jié)
倉儲作為領(lǐng)域模型和數(shù)據(jù)模型的中介,它負責(zé)映射領(lǐng)域模型到持久化存儲。
倉儲實現(xiàn)了透明持久化,即領(lǐng)域?qū)硬恍枰P(guān)注領(lǐng)域?qū)ο笕绾纬志没?/p>
倉儲是一個契約,而不是數(shù)據(jù)訪問層。它明確表明聚合所必需的數(shù)據(jù)操作。
ORM框架不是倉儲。倉儲是一種架構(gòu)模式。ORM用來以面向?qū)ο蟮姆绞絹肀硎緮?shù)據(jù)模型。倉儲使用ORM來協(xié)調(diào)領(lǐng)域模型和數(shù)據(jù)模型。
倉儲適用于具有豐富領(lǐng)域模型的限界上下文。對于沒有復(fù)雜業(yè)務(wù)邏輯的簡單限界上下文,直接使用持久化框架即可。
使用UOW進行事務(wù)管理。UOW負責(zé)跟蹤對象的狀態(tài),倉儲在UOW協(xié)調(diào)的事務(wù)中進行實際的持久化工作。
倉儲用于管理單個聚合,它不應(yīng)該控制事務(wù)。
參考資料:
領(lǐng)域驅(qū)動設(shè)計(DDD)的實踐經(jīng)驗分享之持久化透明
Repository Pattern--A data persistence abstraction
領(lǐng)域驅(qū)動設(shè)計(DDD)的實踐經(jīng)驗分享之ORM的思考
相關(guān)文章
DDD理論學(xué)習(xí)系列(1)-- 通用語言
DDD領(lǐng)域驅(qū)動之干貨 (一)
DDD理論學(xué)習(xí)系列(2)-- 領(lǐng)域
DDD理論學(xué)習(xí)系列(3)-- 限界上下文
DDD理論學(xué)習(xí)系列(4)-- 領(lǐng)域模型
事件總線知多少(2)
DDD理論學(xué)習(xí)系列(5)-- 統(tǒng)一建模語言
DDD理論學(xué)習(xí)系列(6)-- 實體
DDD理論學(xué)習(xí)系列(7)-- 值對象
DDD理論學(xué)習(xí)系列(8)-- 應(yīng)用服務(wù)&領(lǐng)域服務(wù)
DDD理論學(xué)習(xí)系列(9)-- 領(lǐng)域事件
DDD理論學(xué)習(xí)系列(10)-- 聚合
DDD理論學(xué)習(xí)系列(11)-- 工廠
從事件和DDD入手來構(gòu)建微服務(wù)
DDD領(lǐng)域驅(qū)動之干貨 (一)
WeText項目:一個基于.NET實現(xiàn)的DDD、CQRS與微服務(wù)架構(gòu)的演示案例
【DDD/CQRS/微服務(wù)架構(gòu)案例】在Ubuntu 14.04.4 LTS中運行WeText項目的服務(wù)端
基于.NET CORE微服務(wù)框架 -surging的介紹和簡單示例 (開源)
剝析surging的架構(gòu)思想
基于.NET CORE微服務(wù)框架 -談?wù)剆urging的服務(wù)容錯降級
我眼中的ASP.NET Core之微服務(wù)
.NET Core 事件總線,分布式事務(wù)解決方案:CAP
原文地址:http://www.cnblogs.com/sheng-jie/p/7261286.html
.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔為你收集整理的DDD理论学习系列(12)-- 仓储的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MassTransitamp;amp;S
- 下一篇: .NET Core引入性能分析引导优化