1. 引言
DDD中的Repository,主要有兩種翻譯:資源庫和倉儲(chǔ)尚胞,本文取倉儲(chǔ)之譯喻粹。
說到倉儲(chǔ),我們肯定就想到了倉庫井佑,倉庫一般用來存放貨物属铁,而倉庫一般由倉庫管理員來管理。當(dāng)工廠生產(chǎn)了一批貨物時(shí)躬翁,只需交給倉庫管理員即可红选,他負(fù)責(zé)貨物的堆放;當(dāng)需要發(fā)貨的時(shí)候姆另,倉庫管理員負(fù)責(zé)從倉庫中撿貨進(jìn)行貨物出庫處理。當(dāng)需要庫存盤點(diǎn)時(shí)坟乾,倉庫管理員負(fù)責(zé)核實(shí)貨物狀態(tài)和庫存迹辐。換句話說,倉庫管理員負(fù)責(zé)了貨物的出入庫管理甚侣。通過倉庫管理員這個(gè)角色明吩,保證了倉庫和工廠的獨(dú)立性,工廠只需要負(fù)責(zé)生產(chǎn)即可殷费,而至于貨物如何存放工廠無需關(guān)注印荔。
而我們要講的倉儲(chǔ)就類似于倉庫管理員,只不過它負(fù)責(zé)的不再是貨物的管理详羡,而是聚合的管理仍律,倉儲(chǔ)介于領(lǐng)域模型和數(shù)據(jù)模型之間,主要用于聚合的持久化和檢索实柠。它隔離了領(lǐng)域模型和數(shù)據(jù)模型水泉,以便我們關(guān)注于領(lǐng)域模型而不需要考慮如何進(jìn)行持久化。
2. DDD中的倉儲(chǔ)
2.1. 倉儲(chǔ)的集合特性
倉儲(chǔ)代表一個(gè)聚合的集合,其行為與.Net集合一樣草则,倉儲(chǔ)用來存儲(chǔ)和刪除聚合钢拧,但同時(shí)提供針對(duì)聚合的顯式查詢以及匯總。
2.2. 倉儲(chǔ)與數(shù)據(jù)訪問層的區(qū)別
- 倉儲(chǔ)限定了只能通過聚合根來持久化和檢索領(lǐng)域?qū)ο罂缓幔源_保所有改動(dòng)和不變性由聚合處理源内。
- 倉儲(chǔ)通過隱藏聚合持久化和檢索的底層技術(shù)實(shí)現(xiàn)領(lǐng)域?qū)拥牡某志没療o關(guān)性(即領(lǐng)域?qū)硬恍枰廊绾纬志没I(lǐng)域?qū)ο螅?/li>
- 倉儲(chǔ)在數(shù)據(jù)模型和領(lǐng)域模型定義了一個(gè)邊界。
2.3. 倉儲(chǔ)舉例
下面我們首先來看一個(gè)簡單倉儲(chǔ)的定義:
namespace DomainModel
{
public interface ICustomerRepository
{
Customer FindBy(Guid id);
void Add(Customer customer);
void Remove(Customer customer);
}
}
通常來說份殿,倉儲(chǔ)由應(yīng)用服務(wù)層調(diào)用膜钓。倉儲(chǔ)定義應(yīng)用服務(wù)執(zhí)行業(yè)務(wù)用例時(shí)需要的所有的數(shù)據(jù)訪問方法。而倉儲(chǔ)的實(shí)現(xiàn)通常位于基礎(chǔ)架構(gòu)層伯铣,由持久化框架來支撐呻此。以下的倉儲(chǔ)實(shí)現(xiàn)是借助于ORM框架Nhibernate的ISession
接口,它扮演一個(gè)的網(wǎng)關(guān)角色腔寡,負(fù)責(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. 倉儲(chǔ)的誤解
倉儲(chǔ)也存在很多誤解凭语,許多人認(rèn)為其是不必要的抽象葱她。當(dāng)應(yīng)用于簡單的領(lǐng)域模型時(shí),可以直接使用持久化框架來進(jìn)行數(shù)據(jù)訪問似扔。然而當(dāng)對(duì)復(fù)雜的領(lǐng)域模型進(jìn)行建模時(shí)吨些,倉儲(chǔ)是模型的擴(kuò)展,它表明聚合檢索的意圖炒辉,可以對(duì)領(lǐng)域模型進(jìn)行有意義的讀寫豪墅,而不是一個(gè)技術(shù)框架。
也有很多人認(rèn)為倉儲(chǔ)是一種反模式黔寇,因?yàn)槠潆[藏了基礎(chǔ)持久化框架的功能偶器。而恰巧這正是倉儲(chǔ)的要點(diǎn)》炜悖基礎(chǔ)持久化框架提供了開放的接口用于對(duì)數(shù)據(jù)模型的查找和修改屏轰,而倉儲(chǔ)通過使用定義的命名查詢方法來限制對(duì)聚合的訪問。通過使查詢顯式化憋飞,就更容易調(diào)整查詢霎苗,且更重要的是倉儲(chǔ)明確了查詢的意圖,便于領(lǐng)域?qū)<依斫獠笳浮Ee個(gè)例子:我們?cè)趥}儲(chǔ)中定義了一個(gè)方法GetAllActiveUsers()
與sql語句select * from users where isactive = 1
或var users =db.Users.Where(u=>u.IsActive ==1)
相比叨粘,很明顯倉儲(chǔ)的方法命名就能讓我們明白了查詢的意圖:查詢所有處于Active狀態(tài)的用戶猾编。除了查詢,倉儲(chǔ)僅暴露必要的持久化方法而不是提供所有的CURD方法升敲。
2.5. 倉儲(chǔ)的要點(diǎn)
倉儲(chǔ)的要點(diǎn)并不是使代碼更容易測(cè)試答倡,也不是為了便于切換底層的持久化存儲(chǔ)方式。當(dāng)然驴党,在某種程度上瘪撇,這也的確是倉儲(chǔ)所帶來的利好。倉儲(chǔ)的要點(diǎn)是保持你的領(lǐng)域模型和技術(shù)持久化框架的獨(dú)立性港庄,這樣你的領(lǐng)域模型可以隔離來自底層持久化技術(shù)的影響倔既。如果沒有倉儲(chǔ)這一層,你的持久化基礎(chǔ)設(shè)施可能會(huì)泄露到領(lǐng)域模型中鹏氧,并影響領(lǐng)域模型完整性和最終一致性渤涌。
3. 領(lǐng)域模型 VS 數(shù)據(jù)模型
如果選擇關(guān)系型數(shù)據(jù)庫作為持久化存儲(chǔ),我們可以借助于ORM框架來實(shí)現(xiàn)領(lǐng)域模型和數(shù)據(jù)模型之間的映射和持久化操作把还。
而ORM又是什么呢实蓬?
按照文章開頭中的例子,如果倉儲(chǔ)對(duì)應(yīng)倉庫管理員的角色吊履,那ORM就相當(dāng)于倉庫機(jī)器人安皱,而倉庫就相當(dāng)于數(shù)據(jù)庫。為了方便不同商品的歸類存放艇炎,對(duì)倉庫進(jìn)行分區(qū)酌伊,分區(qū)就相當(dāng)于數(shù)據(jù)表。當(dāng)公司接到一筆訂單做發(fā)貨處理時(shí)缀踪,銷售員將發(fā)貨通知單告知倉庫管理員居砖,倉庫管理員再分配ORM機(jī)器人進(jìn)行撿貨。很顯然驴娃,ORM機(jī)器人必須能夠識(shí)別發(fā)貨通知單悯蝉,將發(fā)貨通知單中的商品對(duì)應(yīng)到倉庫中存儲(chǔ)的貨物。這里面發(fā)貨通知單就相當(dāng)于領(lǐng)域模型托慨,而倉庫中存儲(chǔ)的貨物就屬于數(shù)據(jù)模型。
相信基于上面的比喻暇榴,我們對(duì)ORM有了基本的認(rèn)識(shí)厚棵。ORM,全稱是Object Relational Mapping蔼紧,對(duì)象關(guān)系映射婆硬。ORM的前提是,將對(duì)象的屬性映射到數(shù)據(jù)庫字段奸例,將對(duì)象之間的引用映射到數(shù)據(jù)庫表的關(guān)系彬犯。換句話說向楼,ORM負(fù)責(zé)將代碼中定義的對(duì)象和關(guān)系映射到數(shù)據(jù)庫的表結(jié)構(gòu)中去,并在進(jìn)行數(shù)據(jù)訪問時(shí)再將表數(shù)據(jù)映射到代碼中定義的對(duì)象谐区,借助ORM我們不需要去手動(dòng)寫SQL語句就可以完成數(shù)據(jù)的增刪改查湖蜕。ORM僅僅抽象了關(guān)系數(shù)據(jù)模型,它只是以面向?qū)ο蟮姆绞絹肀硎緮?shù)據(jù)模型宋列,以方便我們?cè)诖a中輕松地處理數(shù)據(jù)昭抒。
下面我們來探討一下數(shù)據(jù)模型與領(lǐng)域模型的異同。關(guān)系數(shù)據(jù)庫中的數(shù)據(jù)模型炼杖,它由表和列組成灭返,它只是簡單的存儲(chǔ)結(jié)構(gòu),用于保存領(lǐng)域模型某個(gè)時(shí)間點(diǎn)的狀態(tài)坤邪。數(shù)據(jù)模型可以分散在幾個(gè)表甚至幾個(gè)數(shù)據(jù)庫中熙含。此外,可以使用多種形式的持久化存儲(chǔ)艇纺,例如文件怎静、web服務(wù)器、關(guān)系數(shù)據(jù)庫或NoSQL喂饥。領(lǐng)域模型是對(duì)問題域的抽象消约,具有豐富的語言和行為,由實(shí)體和值對(duì)象組成员帮。對(duì)于一些領(lǐng)域模型或粮,可能與數(shù)據(jù)模型相似,甚至相同捞高,但在概念上它們是非常不同的氯材。ORM與領(lǐng)域模型無關(guān)。倉儲(chǔ)的作用就是將領(lǐng)域模型與數(shù)據(jù)模型分開硝岗,而不是讓它們模糊成一個(gè)模型氢哮。ORM不是倉儲(chǔ),但是倉儲(chǔ)可以使用ORM來持久化領(lǐng)域?qū)ο蟮臓顟B(tài)型檀。
如果你的領(lǐng)域模型與你的數(shù)據(jù)模型類似冗尤,ORM可以直接映射領(lǐng)域模型到數(shù)據(jù)存儲(chǔ),否則胀溺,則需要對(duì)ORM進(jìn)行額外的映射配置裂七。
4. 倉儲(chǔ)的定義和實(shí)現(xiàn)
上面也提到過,我們一般在領(lǐng)域?qū)佣x倉儲(chǔ)接口仓坞,在基礎(chǔ)設(shè)施層實(shí)現(xiàn)倉儲(chǔ)背零,以隔離領(lǐng)域模型和數(shù)據(jù)模型。
4.1. 倉儲(chǔ)方法需明確
倉儲(chǔ)是原則上是領(lǐng)域模型與持久化存儲(chǔ)之間明確的契約无埃,倉儲(chǔ)定義的接口方法不僅僅是CURD方法徙瓶。它是領(lǐng)域模型的擴(kuò)展毛雇,并以領(lǐng)域?qū)<宜斫獾男g(shù)語編寫。倉儲(chǔ)接口的定義應(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);
}
}
以上倉儲(chǔ)定義了一個(gè)FindAllThatMatch
方法以支持客戶端以任何方式查詢領(lǐng)域?qū)ο蟆_@個(gè)方法的設(shè)計(jì)思想無可置否虽缕,靈活且可以擴(kuò)展始藕,但是它并沒有明確的表明查詢的意圖,我們就失去了對(duì)查詢的控制氮趋。為了真正了解如何使用這些方法伍派,開發(fā)人員需要跟蹤相關(guān)調(diào)用堆棧,才能知悉方法的意圖剩胁,更別說出現(xiàn)性能問題時(shí)如何著手優(yōu)化了诉植。因?yàn)閭}儲(chǔ)定義的接口方法過于寬泛且不具體,它模糊了領(lǐng)域的的概念昵观,所以定義這樣的一個(gè)接口方法是無意義的晾腔。
我們可以如下改造:
namespace DomainModel {
public interface ICustomerRepository {
Customer FindBy (Guid id);
IEnumerable<Customer> FindAllThatAreDeactivated ();
IEnumerable<Customer> FindAllThatAreOverAllowedCredit ();
void Add (Customer customer);
}
}
通過以上改造,我們通過方法的命名來明確查詢的意圖啊犬,符合通用語言的規(guī)范灼擂。
4.2. 泛型倉儲(chǔ)
在實(shí)踐中我們可能會(huì)發(fā)現(xiàn),為每一個(gè)聚合定義一個(gè)倉儲(chǔ)會(huì)導(dǎo)致重復(fù)代碼觉至,因?yàn)榇蟛糠值臄?shù)據(jù)操作都是類似的剔应。為了代碼重用,泛型倉儲(chǔ)就應(yīng)時(shí)而生语御。
泛型倉儲(chǔ)舉例:
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; }
}
}
泛型倉儲(chǔ)實(shí)現(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 ();
}
}
}
通過定義泛型倉儲(chǔ)和默認(rèn)的實(shí)現(xiàn)峻贮,很大程度上進(jìn)行了代碼重用。但是应闯,嘗試將泛型倉儲(chǔ)應(yīng)用所有倉儲(chǔ)并不是一個(gè)好的主意纤控。對(duì)于簡單的聚合我們可以直接使用泛型倉儲(chǔ)來簡化代碼。但對(duì)于復(fù)雜的聚合碉纺,泛型倉儲(chǔ)可能就會(huì)不太適合船万,如果基于泛型倉儲(chǔ)的方法進(jìn)行數(shù)據(jù)訪問,就會(huì)模糊對(duì)聚合的訪問意圖骨田。
對(duì)于復(fù)雜的聚合唬涧,我們可以重新定義:
namespace DomainModel {
public interface ICustomerRepository {
Customer FindBy (Guid id);
IEnumerable<Customer> FindAllThatAreDeactivated ();
void Add (Customer customer);
}
}
在實(shí)現(xiàn)時(shí),我們可以引用泛型倉儲(chǔ)來避免代碼重復(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
在定義倉儲(chǔ)方法的返回值時(shí)狮荔,我們可能會(huì)比較疑惑,是應(yīng)該直接返回?cái)?shù)據(jù)(IEnumerable)還是返回查詢(IQueryable)以便進(jìn)行進(jìn)一步的細(xì)化查詢介粘?返回IEnumerable
會(huì)比較安全殖氏,但IQueryable
提供了更好的靈活性。事實(shí)上姻采,如果使用IQueryable
作為返回值雅采,我們僅提供一種讀取數(shù)據(jù)的方法即可進(jìn)行各種查詢。
但是這種方式就會(huì)引入一個(gè)問題慨亲,就是業(yè)務(wù)邏輯會(huì)滲透到應(yīng)用層中去婚瓜,并出現(xiàn)大量重復(fù)。比如刑棵,在實(shí)體中我們一般使用IsActive
或IsDeleted
屬性來表示軟刪除巴刻,而一旦實(shí)體中的某條數(shù)據(jù)被刪除,那么UI中基本不會(huì)再顯示這條數(shù)據(jù)蛉签,那對(duì)于實(shí)體的查詢都需要包含類似Where(c=> c.IsActive)
的linq表達(dá)式胡陪。對(duì)于這種問題,我們最好在倉儲(chǔ)中的方法中碍舍,比如List()
或者ListActive()
做默認(rèn)處理柠座,而不是在應(yīng)用服務(wù)層每次去指定查詢條件。
但具體是返回 IQueryable還是IEnumerable每個(gè)人的看法不一片橡,具體可參考Repository 返回 IQueryable妈经?還是 IEnumerable?锻全。
5. 事務(wù)管理和工作單元
事務(wù)管理主要是應(yīng)用服務(wù)層的關(guān)注點(diǎn)狂塘。然而,因?yàn)閭}儲(chǔ)和事務(wù)管理緊密相關(guān)的鳄厌。倉儲(chǔ)僅關(guān)注單一聚合的管理荞胡,而一個(gè)業(yè)務(wù)用例可能會(huì)涉及到多種的聚合。
事務(wù)管理由UOW(Unit of Work)處理了嚎。UOW模式的作用是在業(yè)務(wù)用例的操作中跟蹤聚合的所有更改泪漂。一旦發(fā)生了更改,UOW就使用事務(wù)來協(xié)調(diào)持久化存儲(chǔ)歪泳。為了確保數(shù)據(jù)的完整性萝勤,如果提交數(shù)據(jù)失敗,則會(huì)回滾所有更改呐伞,以確保數(shù)據(jù)保持有效狀態(tài)敌卓。
而關(guān)于UOW又是一個(gè)復(fù)雜的話題,我們后續(xù)再講伶氢。
6. 倉儲(chǔ)的反模式(注意事項(xiàng))
不要支持臨時(shí)查詢(ad hoc query)
倉儲(chǔ)不應(yīng)該開放擴(kuò)展趟径,不要為了支持多種形式的查詢瘪吏,定義比較寬泛的查詢方法,它不僅不能明確表達(dá)倉儲(chǔ)查詢的意圖蜗巧,更可能會(huì)導(dǎo)致查詢性能掌眠。延遲加載是一種設(shè)計(jì)臭味
聚合應(yīng)圍繞不變性構(gòu)建,并包含所有必需的屬性去支持不變性幕屹。 因此蓝丙,當(dāng)加載聚合時(shí),要么加載所有望拖,要么一個(gè)也不加載渺尘。 如果您有一個(gè)關(guān)系數(shù)據(jù)庫并且正在使用ORM作為數(shù)據(jù)模型,那么您可能能夠延遲加載一些領(lǐng)域?qū)ο髮傩钥坑椋@樣就可以推遲加載不需要的聚合部分沧烈。但是,這樣做的問題是像云,如果您只能部分加載聚合锌雀,可能會(huì)導(dǎo)致您的聚合邊界錯(cuò)誤。不要使用聚合來實(shí)現(xiàn)報(bào)表需求
報(bào)表可能會(huì)涉及到多個(gè)類型的聚合迅诬,而倉儲(chǔ)是處理單一聚合的腋逆。另外倉儲(chǔ)是基于事務(wù)的,可能會(huì)導(dǎo)致報(bào)表的性能問題侈贷。
7. 總結(jié)
- 倉儲(chǔ)作為領(lǐng)域模型和數(shù)據(jù)模型的中介惩歉,它負(fù)責(zé)映射領(lǐng)域模型到持久化存儲(chǔ)。
- 倉儲(chǔ)實(shí)現(xiàn)了透明持久化俏蛮,即領(lǐng)域?qū)硬恍枰P(guān)注領(lǐng)域?qū)ο笕绾纬志没?/li>
- 倉儲(chǔ)是一個(gè)契約撑蚌,而不是數(shù)據(jù)訪問層。它明確表明聚合所必需的數(shù)據(jù)操作搏屑。
- ORM框架不是倉儲(chǔ)争涌。倉儲(chǔ)是一種架構(gòu)模式。ORM用來以面向?qū)ο蟮姆绞絹肀硎緮?shù)據(jù)模型辣恋。倉儲(chǔ)使用ORM來協(xié)調(diào)領(lǐng)域模型和數(shù)據(jù)模型亮垫。
- 倉儲(chǔ)適用于具有豐富領(lǐng)域模型的限界上下文。對(duì)于沒有復(fù)雜業(yè)務(wù)邏輯的簡單限界上下文伟骨,直接使用持久化框架即可饮潦。
- 使用UOW進(jìn)行事務(wù)管理。UOW負(fù)責(zé)跟蹤對(duì)象的狀態(tài)携狭,倉儲(chǔ)在UOW協(xié)調(diào)的事務(wù)中進(jìn)行實(shí)際的持久化工作继蜡。
- 倉儲(chǔ)用于管理單個(gè)聚合,它不應(yīng)該控制事務(wù)。
參考資料:
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)的實(shí)踐經(jīng)驗(yàn)分享之持久化透明
Repository Pattern--A data persistence abstraction
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)的實(shí)踐經(jīng)驗(yàn)分享之ORM的思考