謹以本文向我腦海中那些不成熟的想法致敬。javascript
受疫情影響呆在家中的這段時間裏,我收尾了《Clean Architecture》。這本書給了我許多新知識和啓發,包括本文的中心論點——數據庫schema不是CRUD服務的一切,也是在讀書過程當中想到的。在書中,做者的原話是java
But the database is not the data model
它出如今書中第六部分《Details》的第一個章節中。做者認爲,從架構的角度來看,數據庫不是一個實體而是一個細節,不足以成爲架構中的一個元素。他甚至打了個比方:數據庫對於架構而言,就好像門把手對於房子通常。而且,做者進一步澄清了他的觀點:他口中所說的數據庫,不是指的數據模型。應用內的數據結構對架構而言相當重要,但數據庫並非數據模型。node
這不由讓我回憶起了本身早期寫設計文檔的套路。git
在個人從業生涯早期(說得好像我從業好久了同樣),每當須要開發一個新的Web服務時,必須先寫一份簡要的設計文檔,向上級清楚地表達個人實現思路,包括:程序員
那時候的我會先考慮數據的存儲結構,而後定義接口,最後纔是與其它服務的協做。這些早期設計文檔的其中一個特色是:接口的響應格式,與數據的存儲結構是相同的。github
比方說我要設計一個網上商城的訂單服務,可能會提供以下查詢特定訂單的接口算法
GET /order/:id
其響應格式可能以下mongodb
{ order: { id: 'F122663A-A5DC-451A-9B79-92DCE2EE41F1', price: '100.00', products: [ { id: 1, name: 'MacBook Pro', price: '19999.00' }, { id: 2, name: 'iPhone', price: '6999.00' } ] } }
爲了保存這種「不平坦」的對象,將會用MongoDB做爲存儲——除了文檔的主鍵_id
以外,其它字段在接口和存儲之間一一對應。數據庫
不只僅是響應格式,在這個Web服務內所操做的也是一樣結構的對象。用MongoDB Node.JS Driver得到的訂單剛好是一個JS對象,它與collection的文檔有着如出一轍的結構。以後這些對象會在代碼內處處流通,不加修飾地使用。定義了數據庫schema(就算是用MongoDB也有一套腦內的schema)後,其它的一切也就跟着肯定了。數組
業界甚至有工具能夠直接從數據庫獲得API,好比postgrest。
不少時候,數據庫schema成了一個應用內事實上的數據模型。可是,即使它們能夠偶然同樣,也不要認爲它們總應該同樣。
以MySQL爲例,在CREATE TABLE
語句中某一列的類型實際上決定的是存儲時分配的空間的多少。但適合存儲的類型,並不必定也適合業務邏輯的運算。
好比說,要在MySQL中存儲「開關型」的數據,即諸如「是否啓用」或「是否已支付」這樣非此即彼的狀態時,一般定義爲TINYINT
類型,用0表示邏輯假(「未啓用」和「未支付」),1表示邏輯真(「已啓用」和「已支付」)。但對代碼而言,比起用數值類型,布爾類型纔是更恰當的選擇。尤爲是當所選擇的語言並無將0與false
、1與true
等價起來的時候——在Common Lisp中,(if 0 1 2)
的求值結果爲1。
一般數據結構在內存中比在磁盤上要容易表達得多,因此代碼中使用的數據結構會比數據庫中存儲的要靈活很多,這一樣形成了二者的不匹配。
以我本身開發的提醒工具cuckoo
爲例,應用內有兩種對象:任務和提醒。任務描述了要作的事情,提醒描述了在何時該告訴用戶。顯然,提醒是一個依賴於任務的弱實體。在cuckoo的代碼中,任務是Task
類的實例對象,有一個名爲remind
的成員變量存儲着提醒。
但這樣的結構不方便存儲在MySQL中。遵守關係型數據庫設計的第一範式,任務和提醒分別被存儲在t_task
和t_remind
表中,二者經過t_task.remind_id
聯繫起來。
固然,也能夠在一開始就用MongoDB來存儲這些數據(甚至能夠用對象數據庫?不過我沒玩過)。尤爲是cuckoo只是一個小玩意兒,MySQL和MongoDB都足以勝任。但做爲一名有理想的程序員,在作設計的時候,不該該讓低層細節過度干預高層策略。(在《Clean Architecture》中,越是接近I/O的越是low-level,反之則是high-level。)
業務邏輯和規則纔是一個服務的核心,應該把更多精力花在實現業務邏輯的數據結構和算法上。
以網上商城中常見的優惠券功能爲例。優惠券服務所管理的優惠券每每有着各類效果、條件,以及限制。爲了保持靈活性,優惠券類(下稱Coupon
)的實例對象中會有三種接口類型的成員變量:
Effect
類型的變量effect
,負責實現優惠效果的計算邏輯;Condition
數組類型的變量conditions
,負責實現使用條件的檢查邏輯;Restriction
數組類型的變量restrictions
,負責實現使用限制的檢查邏輯。三個接口能夠有各類各樣的實現——定額減免、折扣減免、某年月日前可用、不可用於電子產品,等等。如此,優惠券功能具有了極大的靈活性,業務能夠爲所欲爲,產品能夠隨心所欲,老闆數錢數到手軟,公司業績蒸蒸日上。
那麼如何存儲Effect
、Condition
、Restriction
、Coupon
類的實例對象呢?沒有惟一的選擇,既能夠存儲在MySQL中,也能夠存儲在MongoDB中,或者別的什麼數據庫中。無論這些數據最終如何持久化,都不會影響做爲高層策略的優惠券業務邏輯。反過來,若是在代碼中處理的不是類、接口,以及實例對象,而是直接從數據庫中取出來的、貧血模型的行(或文檔),處理起來就不是很優雅了——能夠預見 ,代碼中會充斥着許多的if-else
判斷邏輯。
數據庫只是幫忙從磁盤中讀取數據的軟件,它的schema不該該直接成爲應用的數據模型。
不該該在HTTP接口的響應中直接暴露數據庫的schema。
不說別的,光是數據庫schema與接口規格所使用的命名規則就足以形成差別了。也許在MySQL中用snake case
命名一列,卻又在HTTP響應的JSON對象中用camel case
命名字段。
此外,除非這些接口僅僅實現增刪查改、沒有任何的業務邏輯或規則,不然一個服務更應當提供與業務需求剛好契合的接口。仍然以上文的優惠券服務爲例,儘管內部可能Effect
、Condition
、Restriction
、Coupon
等諸多概念,但煮不在意用戶不在意,他們只想看到用人話說出來的優惠券效果以及使用規則——用戶甚至不關心條件和限制有何不一樣。
若是優惠券服務直接將數據庫中的行(或文檔)序列化成JSON返回給調用者,會致使封裝的泄露。每個查詢優惠券的調用方,都必須瞭解優惠券的內部表示形式,必須知道效果由effect
描述、用券後的訂單金額是多少、conditions
中有關於過時與否的信息,等等。每增長一個優惠券服務的使用者,就相應地增長一套描述這些內容的代碼。甚至當優惠券服務自身重構的時候,也許牽連到衆多的調用方。
若是直接將存儲結構暴露給調用者的話,又何須再作一個Web服務呢。
的確存在這樣的例子,數據庫schema、數據模型,以及HTTP響應結構三者相同。這是由於比起維護數據庫schema與數據模型的轉換規則,以及DTO與數據模型的轉換規則而言,在領域代碼中直接使用數據庫schema來表達數據模型的成本更低一點。儘管數據庫schema不是Web服務的一切,但不少時候能夠因地制宜地妥協一下。