利用聚合概念指導MongoDB的Schema設計

習慣的力量強大卻往往無法察覺。往往不經意之間,陷入習慣的陷阱中卻不自知。

在我們的項目中,為了能夠保存分析報表以及用戶設置的報表查詢條件,我們將這些信息視為報表元數據存儲在MongoDB中。要存儲的元數據包括:

  • 報表分類(ReportCategory)

  • 報表(Report)

  • 報表查詢條件(QeuryCondition)

一個報表分類會包含多個報表,同一個報表只能屬於一個分類。每個報表提供了多個標準查詢條件和多個用戶自定義查詢條件。

利用聚合概念指導MongoDB的Schema設計

我需要為這些元數據設計MongoDB的DB Schema。最初考慮將這三個概念合起來定義為元數據表的一條記錄。之後想到對於一個報表而言,需要頻繁對報表的查詢條件進行增刪操作,似乎又應該將查詢條件單獨分離出來。那麼報表分類與報表呢?是否將報表也獨立出來才合適?對於MongoDB這樣的Document數據庫而言,將Report作為ReportCategory的embedded屬性也是可行的,至少不會像關係型數據庫那樣會產生數據冗餘。倘若要分開,當需要查詢某個分類下的所有報表時,還得多餘地做一次Link。

好糾結啊!似乎怎麼設計都是可行的,又彷彿總有不如意處。

正在思索中,突然想起對於這樣面向文檔的NoSQL數據庫而言,使用聚合(Aggregate)來觀察表記錄會更加恰當。這個想法恍若閃電般迅捷而銳利,猛地撞向腦中的思緒,一下子點燃了我的設計思維。

這裡所謂“聚合”,非面向對象中表達對象關係的概念,而是領域驅動設計(DDD)對對象邊界的思考。關於聚合(Aggregate)的設計,我根據過往的經驗,整理出五條設計原則:

  • 聚合作為一種邊界,主要用於維護業務完整性,此時應遵循業務規則中定義的不變量(Invariant)

  • 作為聚合邊界內的非聚合根實體對象,若可能被別的調用者單獨調用,則應該作為單獨的聚合分離出來

  • 在聚合邊界內的非聚合根對象,與聚合根之間應該存在直接或間接的引用關係,且可以通過對象的引用方式;若必須採用Id來引用,則說明被引用的對象不屬於該聚合

  • 若一個對象缺少另一個對象作為其主對象就不可能存在,則該對象一定屬於該主對象的聚合邊界內

  • 若一個實體對象,可能被多個聚合引用,則該實體對象應首先考慮作為單獨的聚合

這些設計原則都是我在探索聚合設計時的一些思考,多次實踐下來,竊以為頗有指導價值。這裡不再鋪開,留待以後的文章詳述。單說本例,我們該如何運用這些原則來思考ReportCategory、Report與QueryCondition之間的關係?

顯然,套用這些原則,我認為前面糾纏不清的混亂思路已可迎刃而解。從業務完整性看,Report雖屬於ReportCategory,但二者未嘗有強的約束關係,即不存在業務上的不變量(Invariant)。例如ReportCategory可以沒有Report,成為一個空的分類,我們也可以撇開ReportCategory,單獨查詢所有的Report。倘若我們將Report放到ReportCategory聚合中,由於Report可能會被單獨調用,聚合的邊界保護反而成為了障礙,不合理。

於是,我們可以得出第一個結論:ReportCategory和Report應該屬於兩個不同的聚合。

基於第四條原則,我們可以提出問題:當QueryCondition缺少Report對象後,還有存在意義嗎?答案一目瞭然,沒有Report,就沒有QueryCondition。皮之不存毛將焉附!第二個結論自然得來:Report與QueryCondition應屬於同一個聚合。於是,模型呼之欲出:

利用聚合概念指導MongoDB的Schema設計

上圖是領域模型而非數據模型。站在領域驅動設計的角度,這才是正確的打開姿勢。那麼,使用該領域模型去指導MongoDB的Schema設計,是否有將領域混入技術實現之嫌呢?從設計方向看,先考慮領域模型才是正解,DB的技術實現應為了滿足該領域模型而設計。只有當領域模型可能阻礙技術實現,又或者依據領域模型得到的Schema設計不滿足性能或其他質量屬性需求時,才應該反過來調整領域模型。對於MongoDB這種面向Document的數據庫,以聚合概念指導Schema設計,可謂水到渠成,不僅沒有違和之感,反而讓Repository的實現變得更加簡單、自然。

在項目開發過程中,我先入為主地做了技術選型,從而習慣性地開始針對MongoDB進行Schema設計,反而忘了領域驅動設計的指導原則。技術人員對技術實現往往見獵心喜,因而忽略了領域設計的驅動力,慎之慎之!


分享到:


相關文章: