一文讀懂,DDD 落地數據庫設計實戰

作者範鋼,曾任航天信息首席架構師,《大話重構》一書的作者。本文根據具體實例詳細描述了 DDD 落實到數據庫設計的整個過程

過去,系統的軟件設計是以數據庫設計爲核心,當需求確定下來以後,團隊首先開始進行數據庫設計。因爲數據庫是各個模塊唯一的接口,當整個團隊將數據庫設計確定下來以後,就可以按照模塊各自獨立地進行開發了,如下圖所示。

在上面的過程中,爲了提高團隊開發速度,儘量讓各個模塊不要交互,從而達到各自獨立開發的效果。但是,隨着系統規模越來越大,業務邏輯越來越複雜,我們越來越難於保證各個模塊獨立不交互了。

隨着軟件業的不斷髮展,軟件系統變得越來越複雜,各個模塊間的交互也越來越頻繁,這時,原有的設計過程已經不能滿足我們的需要了。因爲如果要先進行數據庫設計,但數據庫設計只能描述數據結構,而不能描述系統對這些數據結構的處理。因此,在第一次對整個系統的梳理過程中,只能梳理系統的所有數據結構,形成數據庫設計;接着,又要再次梳理整個系統,分析系統對這些數據結構的處理過程,形成程序設計。爲什麼不能一次性地把整個系統的設計梳理到位呢?

現如今,我們已經按照面向對象的軟件設計過程來分析設計系統了。當開始需求分析時,首先進行用例模型的設計,分析整個系統要實現哪些功能;接着進行領域模型的設計,分析系統的業務實體。在領域模型分析中,採用類圖的形式,每個類可以通過它的屬性來表述數據結構,又可以通過添加方法來描述對這個數據結構的處理。因此,在領域模型的設計過程中,既完成了對數據結構的梳理,又確定了系統對這些數據結構的處理,這樣就把兩項工作一次性地完成了。

在這個設計過程中,其核心是領域模型的設計。以領域模型作爲核心,可以指導系統的數據庫設計與程序設計,此時,數據庫設計就弱化爲了領域對象持久化設計的一種實現方式。

領域對象持久化的思想

什麼叫領域對象的持久化呢?在當今軟件架構設計的主流思想中,面向對象設計成了主流思想,在整個系統運行的過程中,所有的數據都是以領域對象的形式存在的。譬如:

假如我們的服務器是一臺超級強大的服務器,那實際上不需要任何數據庫,直接操作這些領域對象就可以了,但在現實世界中沒有那麼強大的服務器。因此,必須將暫時不用的領域對象持久化存儲到磁盤中,而數據庫只是這種持久化存儲的一種實現方式。

按照這種設計思想,我們將暫時不使用的領域對象從內存中持久化存儲到磁盤中。當日後需要再次使用這個領域對象時,根據 key 值到數據庫查找到這條記錄,然後將其恢復成領域對象,應用程序就可以繼續使用它了,這就是領域對象持久化存儲的設計思想。

所以,今天的數據庫設計,實際上就是將領域對象的設計按照某種對應關係,轉換成數據庫的設計。同時,隨着整個產業的大數據轉型,今後的數據庫設計思想也將發生巨大的轉變,有可能數據庫就不一定是關係型數據庫了,也許是 NoSQL 數據庫或者大數據平臺。數據庫的設計也不一定遵循 3NF(第三範式)了,可能會增加更多的冗餘,甚至是寬表。

數據庫設計在發生劇烈的變化,但唯一不變的是領域對象。這樣,當系統在大數據轉型時,可以保證業務代碼不變,變化的是數據訪問層(DAO)。這將使得日後大數據轉型的成本更低,讓我們更快地跟上技術快速發展的腳步。

領域模型的設計

此外,這裏有個有趣的問題值得探討:領域模型的設計到底是誰的職責,是需求分析人員還是設計開發人員?我認爲,它是兩個角色相互協作的產物。而未來敏捷開發的組織形成,團隊將更加扁平化。過去是需求分析人員做需求分析,然後交給設計人員設計開發,這種方式就使得軟件設計質量低下而結構臃腫。未來 “大前端” 的思想將支持更多設計開發人員直接參與需求分析,實現從需求分析到設計開發的一體化組織形式。這樣,領域模型就成爲了設計開發人員快速理解需求的利器。

總之,DDD 的數據庫設計實際上已經變成了:以領域模型爲核心,如何將領域模型轉換成數據庫設計的過程。 那麼怎樣進行轉換呢?在領域模型中是一個一個的類,而在數據庫設計中是一個一個的表,因此就是將類轉換成表的過程。

上圖是一個績效考覈系統的領域模型圖,該績效考覈系統首先進行自動考覈,發現一批過錯,然後再給一個機會,讓過錯責任人對自己的過錯進行申辯。這時,過錯責任人可以填寫一張申辯申請單,在申辯申請單中有多個明細,每個明細對應一個過錯行爲,每個過錯行爲都對應了一個過錯類型,這樣就形成了一個領域模型。

接着,要將這個領域模型轉換成數據庫設計,怎麼做呢?很顯然,領域模型中的一個類可以轉換成數據庫中的一個表,類中的屬性可以轉換成表中的字段。但這裏的關鍵是如何處理類與類之間的關係,如何轉換成表與表之間的關係。這時候,就有 5 種類型的關係需要轉換,即傳統的 4 種關係 + 繼承關係。

傳統的 4 種關係

傳統的關係包含一對一、多對一、一對多、多對多這 4 種,它們既存在於類與類之間,又存在於表與表之間,所以可以直接進行轉換。

1. 一對一關係

在以上案例中,“申辯申請單明細”與 “過錯行爲” 就是一對 “一對一” 關係。在該關係中,一個 “申辯申請單明細” 必須要對應一個 “過錯行爲”,沒有一個“過錯行爲” 的對應就不能成爲一個 “申辯申請單明細”。這種約束在數據庫設計時,可以通過外鍵來實現。但是,一對一關係還有另外一個約束,那就是一個“過錯行爲” 最多隻能有一個 “申辯申請單明細” 與之對應。

也就是說,一個 “過錯行爲” 可以沒有 “申辯申請單明細” 與之對應,但如果有,最多隻能有一個 “申辯申請單明細” 與之對應,這個約束暗含的是一種唯一性的約束。因此,將過錯行爲表中的主鍵,作爲申辯申請單明細表的外鍵,並將該字段升級爲申辯申請單明細表的主鍵。

2. 多對一關係

是日常的分析設計中最常見的一種關係。在以上案例中,一個過錯行爲對應一個稅務人員、一個納稅人與一個過錯類型;同時,一個稅務人員,或納稅人,或過錯類型,都可以對應多個過錯行爲。它們就形成了 “多對一” 關係。在數據庫設計時,通過外鍵就可以建立這種 “多對一” 關係。因此,我們進行了如下數據庫的設計:

多對一關係在數據庫設計上比較簡單,然而落實到程序設計時,需要好好探討一下。比如,以上案例,在按照這樣的方式設計以後,在查詢時往往需要在查詢過錯行爲的同時,顯示它們對應的稅務人員、納稅人與過錯類型。這時,以往的設計是增加一個 join 語句。然而,這樣的設計在隨着數據量不斷增大時,查詢性能將受到極大的影響。

也就是說,join 操作往往是關係型數據庫在面對大數據時最大的瓶頸之一。因此,一個更好的方案就是先查詢過錯行爲表,分頁,然後再補填當前頁的其他關聯信息。這時,就需要在 “過錯行爲” 這個值對象中通過屬性變量,增加對稅務人員、納稅人與過錯類型等信息的引用。

3. 一對多關係

該關係往往表達的是一種主 - 子表的關係。譬如,以上案例中的 “申辯申請單” 與“申辯申請單明細”就是一對 “一對多” 關係。除此之外,訂單與訂單明細、表單與表單明細,都是一對多關係。一對多關係在數據庫設計上比較簡單,就是在子表中增加一個外鍵去引用主表中的主鍵。比如本案例中,申辯申請單明細表通過一個外鍵去引用申辯申請單表中的主鍵,如下圖所示。

除此之外,在程序的值對象設計時,主對象中也應當有一個集合的屬性變量去引用子對象。如本例中,在 “申辯申請單” 值對象中有一個集合屬性去引用“申辯申請單明細”。這樣,當通過申辯申請單號查找到某個申辯申請單時,同時就可以獲得它的所有申辯申請單明細,如下代碼所示:

public class Sbsqd {
    private Set<SbsqdMx> sbsqdMxes;
    public void setSbsqdMxes(Set<SbsqdMx> sbsqdMxes){
          this.sbsqdMxes = sbsqdMxes;
    }
    public Set<SbsqdMx> getSbsqdMxes(){
          return this.sbsqdMxes;
    }
    ……
}

4. 多對多關係

比較典型的例子就是 “用戶角色” 與“功能權限”。一個 “用戶角色” 可以申請多個 “功能權限”;而一個“功能權限” 又可以分配給多個 “用戶角色” 使用,這樣就形成了一個 “多對多” 關係。這種多對多關係在對象設計時,可以通過一個 “功能 - 角色關聯類” 來詳細描述。因此,在數據庫設計時就可以添加一個“角色功能關聯表”,而該表的主鍵就是關係雙方的主鍵進行的組合,形成的聯合主鍵,如下圖所示:

以上是領域模型和數據庫都有的 4 種關係。因此,在數據庫設計時,直接將相應的關係轉換成數據庫設計就可以了。同時,在數據庫設計時還要將它們進一步細化。如在領域模型中,不論對象還是屬性,在命名時都採用中文,這樣有利於溝通與理解。但到了數據庫設計時,就要將它們細化爲英文命名,或者漢語拼音首字母,同時還要確定它們的字段類型與是否爲空等其他屬性。

繼承關係的 3 種設計

第 5 種關係就不太一樣了:繼承關係是在領域模型設計中有,但在數據庫設計中卻沒有。如何將領域模型中的繼承關係轉換成數據庫設計呢?有 3 種方案可以選擇。

1. 繼承關係的第一種方案

首先,看看以上案例。“執法行爲”通過繼承分爲 “正確行爲” 和“過錯行爲”。如果這種繼承關係的子類不多(一般就 2 ~ 3 個),並且每個子類的個性化字段也不多(3 個以內)的話,則可以使用一個表來記錄整個繼承關係。在這個表的中間有一個標識字段,標識表中的每條記錄到底是哪個子類,這個字段的前面部分羅列的是父類的字段,後面依次羅列各個子類的個性化字段。

採用這個方案的優點是簡單,整個繼承關係的數據全部都保存在這個表裏。但是,它會造成 “表稀疏”。在該案例中,如果是一條“正確行爲” 的記錄,則字段 “過錯類型” 與“扣分”永遠爲空;如果是一條 “過錯行爲” 的記錄,則字段 “加分” 永遠爲空。假如這個繼承關係中各子類的個性化字段很多,就會造成該表中出現大量字段爲空,稱爲 “表稀疏”。在關係型數據庫中,爲空的字段是要佔用空間的。因此,這種“表稀疏” 既會浪費大量存儲空間,又會影響查詢速度,是需要極力避免的。所以,當子類比較多,或者子類個性化字段多的情況是不適合該方案(第一種方案)的。

2. 繼承關係的第二種方案

如果執法行爲按照考覈指標的類型進行繼承,分爲 “考覈指標 1”“考覈指標 2”“考覈指標 3”…… 如下圖所示:

並且每個子類都有很多的個性化字段,則採用前面那個方案就不合適了。這時,用另外兩個方案進行數據庫設計。其中一個方案是將每個子類都對應到一個表,有幾個子類就有幾個表,這些表共用一個主鍵,即這幾個表的主鍵生成器是一個,某個主鍵值只能存在於某一個表中,不能存在於多個表中。每個表的前面是父類的字段,後面羅列各個子類的字段,如下圖所示:

如果業務需求是在前端查詢時,每次只能查詢某一個指標,那麼採用這種方案就能將每次查詢落到某一個表中,方案就最合適。但如果業務需求是要查詢某個過錯責任人涉及的所有指標,則採用這種方案就必須要在所有的表中進行掃描,那麼查詢效率就比較低,並不適用。

3. 繼承關係的第三種方案

如果業務需求是要查詢某個過錯責任人涉及的所有指標,則更適合採用以下方案,將父類做成一個表,各個子類分別對應各自的表(如圖所示)。這樣,當需要查詢某個過錯責任人涉及的所有指標時,只需要查詢父類的表就可以了。如果要查看某條記錄的詳細信息,再根據主鍵與類型字段,查詢相應子類的個性化字段。這樣,這種方案就可以完美實現該業務需求。

綜上所述,將領域模型中的繼承關係轉換成數據庫設計有 3 種方案,並且每個方案都有各自的優缺點。因此,需要根據業務場景的特點與需求去評估,選擇哪個方案更適用。

NoSQL 數據庫的設計

前面我們講的數據庫設計,還是基於傳統的關係型數據庫、基於第三範式的數據庫設計。但是,隨着互聯網高併發與分佈式技術的發展,另一種全新的數據庫類型孕育而生,那就是 NoSQL 數據庫。正是由於互聯網應用帶來的高併發壓力,採用關係型數據庫進行集中式部署不能滿足這種高併發的壓力,才使得分佈式 NoSQL 數據庫得到快速發展。

也正因爲如此,NoSQL 數據庫與關係型數據庫的設計套路是完全不同的。關係型數據庫的設計是遵循第三範式進行的,它使得數據庫能夠大幅度降低冗餘,但又從另一個角度使得數據庫查詢需要頻繁使用 join 操作,在高併發場景下性能低下。

所以,NoSQL 數據庫的設計思想就是儘量幹掉 join 操作,即將需要 join 的查詢在寫入數據庫表前先進行 join 操作,然後直接寫到一張單表中進行分佈式存儲,這張表稱爲 “寬表”。這樣,在面對海量數據進行查詢時,就不需要再進行 join 操作,直接在這個單表中查詢。同時,因爲 NoSQL 數據庫自身的特點,使得它在存儲爲空的字段時不佔用空間,不擔心 “表稀疏”,不影響查詢性能。

因此,NoSQL 數據庫在設計時的套路就是,儘量在單表中存儲更多的字段,只要避免數據查詢中的 join 操作,即使出現大量爲空的字段也無所謂了。

增值稅發票票樣圖

正因爲 NoSQL 數據庫在設計上有以上特點,因此將領域模型轉換成 NoSQL 數據庫時,設計就完全不一樣了。比如,這樣一張增值稅發票,如上圖所示,在數據庫設計時就需要分爲發票信息表、發票明細表與納稅人表,而在查詢時需要進行 4 次 join 才能完成查詢。但在 NoSQL 數據庫設計時,將其設計成這樣一張表:

{ _id: ObjectId(7df78ad8902c)
  fpdm: '3700134140', fphm: '02309723‘, 
  kprq: '2016-1-25 9:22:45',
  je: 70451.28, se: 11976.72, 
  gfnsr: {
     nsrsbh: '370112582247803',
     nsrmc:'聯通華盛通信有限公司濟南分公司',…
  },
  xfnsr: {
     nsrsbh: '370112575587500',
     nsrmc:'聯通華盛通信有限公司濟南分公司',…
  },
  spmx: [
     { qdbz:'00', wp_mc:'藍牙耳機 車語者S1 藍牙耳機', sl:2, dj:68.37,… },
     { qdbz:'00', wp_mc:'車載充電器 新在線', sl:1, dj:11.11,… },
     { qdbz:'00', wp_mc:'保護殼 非尼膜屬 iPhone6 電鍍殼', sl:1, dj:24,…  }
  ]
}

在該案例中,對於 “一對一” 和“多對一”關係,在發票信息表中通過一個類型爲 “對象” 的字段來存儲,比如 “購方納稅人(gfnsr)” 與“銷方納稅人(xfnsr)”字段。對於 “一對多” 和“多對多”關係,通過一個類型爲 “對象數組” 的字段來存儲,如 “商品明細(spmx)” 字段。在這樣一個發票信息表中就可以完成對所有發票的查詢,無須再進行任何 join 操作。

同樣,採用 NoSQL 數據庫怎樣實現繼承關係的設計呢?由於 NoSQL 數據庫自身的特點決定了不用擔心 “表稀疏”,同時要避免 join 操作,所以比較適合採用第一個方案,即將整個繼承關係放到同一張表中進行設計。這時,NoSQL 數據庫的每一條記錄可以有不一定完全相同的字段,可以設計成這樣:

{ _id: ObjectId(79878ad8902c),
  name: ‘Jack’,
  type: ‘parent’,
  partner: ‘Elizabeth’,
  children: [
    { name: ‘Tom’, gender: ‘male’ },
    { name: ‘Mary’, gender: ‘female’}
  ]
},
{ _id: ObjectId(79878ad8903d),
  name: ‘Bob’,
  type: ‘kid’,
  mother: ‘Anna’,
  father: ‘David’
}

以上案例是一個用戶檔案表,有兩條記錄:Jack 與 Bob。但是,Jack 的類型是 “家長”,因此其個性化字段是“伴侶” 與“孩子”;而 Bob 的類型是 “孩子”,因此他的個性化字段是“父親” 與“母親”。顯然,在 NoSQL 數據庫設計時就會變得更加靈活。

總結

將領域模型落地到系統設計包含 2 部分內容,本文演練了第一部分內容——從 DDD 落實到數據庫設計的整個過程:傳統的 4 種關係可以直接轉換;繼承關係有 3 種設計方案;轉換成 NoSQL 數據庫則是完全不同的思路。

有了 DDD 的指導,可以幫助我們理清數據間的關係,以及對數據的操作。不僅如此,在未來面對大數據轉型時更加從容。

號主簡介:馮濤,曾任職於阿里巴巴,每日優鮮等互聯網公司,任技術總監,15 年電商互聯網經歷。

最後,將自己 15 年的微服務,高併發,JVM 調優,線上故障排查等經歷整理成電子書送給大家,共 130 頁。絕對乾貨!沒領過的朋友,抓緊啦!

獲取方式:掃描或識別下方二維碼關注公衆號二馬讀書,回覆 **“**電子書

獲取方式:掃描或識別上方二維碼關注公衆號二馬讀書,回覆 **“**電子書

來來來加俺微信,一起交流,共同成長: ftcool2008

原創不易,如果感覺本文對您有幫助,有勞轉發分享或點一下 “在看”!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7gumsj7lHC2BlYqSgLylyw