DDD落地,如何持久化聚合
理解聚合
聚合是一組始終需要保持一致的業(yè)務對象。因此,我們作為一個整體保存和更新聚合,以確保業(yè)務邏輯的一致性。
聚合是 DDD 中最為重要的概念,即使你不使用 DDD 編寫代碼也需要理解這一重要的概念 —— 部分對象的生命周期可以看做一個整體,從而簡化編程。
一般來說,我們需要對聚合內的對象使用 ACID 特性的事務。
最簡單的例子就是訂單和訂單項目,訂單項目更新必須伴隨訂單的更新,否則就會有總價不一致之類的問題。訂單項目需要跟隨訂單的生命周期,我們把訂單叫做聚合根,它就像一個導航員一樣。
- class Order {
- private Collection<OrderItem> orderItems;
- private int totalPrice;
- }
- class OrderItem {
- private String productId;
- private int price;
- private int count;
- }
Order 的 totalPrice 必須是 OrderItem 的 price 之和,還要考慮折扣等其他問題,總之對象的改變都需要整體更新。
理想中最好的方式就是把聚合根整體持久化,不過問題并沒那么簡單。
聚合持久化問題
如果你使用 MySQL 等關系型數(shù)據(jù)庫,集合的持久化是一個比較麻煩的事情:
- 關系的映射不好處理,層級比較深的對象不好轉換。
- 將數(shù)據(jù)轉換為聚合時會有 n+1 的問題,不好使用關系數(shù)據(jù)庫的聯(lián)表特性。
- 全量的數(shù)據(jù)更新數(shù)據(jù)庫的事務較大,性能低下。
- 其他問題
聚合的持久化是 DDD 美好愿景落地的最大攔路虎,這些問題有部分可以被解決而有部分必須取舍。
聚合的持久化到關系數(shù)據(jù)庫的問題,本質是計算機科學的模型問題。
聚合持久化是面向對象模型和關系模型的轉換,這也是為什么 MongoDB 沒有這個問題,但也用不了關系數(shù)據(jù)庫的特性和能力。
面向對象模型關心的是業(yè)務能力承載,關系模型關心的是數(shù)據(jù)的一致性、低冗余。描述關系模型的理論基礎是范式理論,越低的范式就越容易轉換到對象模型。
理論指導實踐,再來分析這幾個問題:
“關系的映射不好處理” 如果我們不使用多對多關系,數(shù)據(jù)設計到第三范式,可以將關系網(wǎng)退化到一顆樹。
△ 網(wǎng)狀的關系
△ 樹狀的關系
"將數(shù)據(jù)轉換為聚合時會有 n+1 的問題" 使用了聚合就不好使用集合的能力,列表查詢可以使用讀模型,直接獲取結果集,也可以利用聚合對緩存的優(yōu)勢使用緩存減輕 n+1 問題。
"全量的數(shù)據(jù)更新數(shù)據(jù)庫的事務較大" 設計小聚合,這是業(yè)務一致性的代價,基本無法避免,但是對于一般應用來說,寫和更新對數(shù)據(jù)庫的頻率并不高。使用讀寫分離即可解決這個問題。
自己實現(xiàn)一個 Repository 層
如果你在使用 Mybatis 或者使用原生的 SQL 來編寫程序,你可以自己抽象一個 Repository 層,這層只提供給聚合根使用,所有的對象都需要使用聚合根來完成持久化。
一種方式是,使用 Mybatis Mapper,對 Mapper 再次封裝。
- class OrderRepository {
- private OrderMapper orderMapper;
- private OrderItemMapper orderItemMapper;
- public Order get(String orderId) {
- Order order = orderMapper.findById(orderId);
- order.setOrderItems(orderItemMapper.findAllByOrderId(orderId))
- return order;
- }
- }
這種做法有一個小點問題,領域對象 Order 中有 orderItems 這個屬性,但是數(shù)據(jù)庫中不可能有 Items,一些開發(fā)者會認為這里的 Order 和通常數(shù)據(jù)庫使用的 OrderEntity 不是一類對象,于是進行繁瑣的類型轉換。
類型轉換和多余的一層抽象,加大了工作量。
如果使用 Mybatis,其實更好的方式是直接使用 Mapper 作為 Repository 層,并在 XML 中使用動態(tài) SQL 實現(xiàn)上述代碼。
還有一個問題是,一對多的關系,發(fā)生了移除操作怎么處理呢?
比較簡單的方式是直接刪除,再存入新的數(shù)組即可,也可以實現(xiàn)對象的對比,有選擇的實現(xiàn)刪除和增加。
完成了這些,恭喜你,得到了一個完整的 ORM,例如 Hibernate 。
使用 Spring Data JPA
所以我們可以使用 JPA 的級聯(lián)更新實現(xiàn)聚合根的持久化。
大家在實際操作中發(fā)現(xiàn),JPA 并不好用。
其實這不是 JPA 的問題,是因為 JPA 做的太多了,JPA 不僅有各種狀態(tài)轉換,還有多對多關系。
如果保持克制就可以使用 JPA 實現(xiàn) DDD,嘗試遵守下面的規(guī)則:
- 不要使用 @ManyToMany 特性
- 只給聚合根配置 Repository 對象。
- 避免造成網(wǎng)狀的關系
- 讀寫分離。關聯(lián)等復雜查詢,讀寫分離查詢不要給 JPA 做,JPA 只做單個對象的查詢
在這些基本的規(guī)則下可以使用 @OneToMany 的 cascade 屬性來自動保存、更新聚合。
- class Order {
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- private String id;
- @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
- @JoinColumn(name = "order_id")
- private Collection<OrderItem> orderItems;
- private int totalPrice;
- }
- class OrderItem {
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- private String id;
- private String productId;
- private int price;
- private int count;
- }
OneToMany 中的 cascade 有不同的屬性,如果需要讓更新、刪除都有效可以設置為 ALL。
使用 Spring Dat JDBC
Mybatis 就是一個 SQL 模板引擎,而 JPA 做的太多,有沒有一個適中的 ORM 來持久化聚合呢?
Spring Data JDBC 就是人們設計出來持久化聚合,從名字來看他不是 JDBC,而是使用 JDBC 實現(xiàn)了部分 JPA 的規(guī)范,讓你可以繼續(xù)使用 Spring Data 的編程習慣。
Spring Dat JDBC 的一些特點:
- 沒有 Hibernate 中 session 的概念,沒有對象的各種狀態(tài)
- 沒有懶加載,保持對象的完整性
- 除了 SPring Data 的基本功能,保持簡單,只有保存方法、事務、審計注解、簡單的查詢方法等。
- 可以搭配 JOOQ 或 Mybatis 實現(xiàn)復雜的查詢能力。
Spring Dat JDBC 的使用方式和 JPA 幾乎沒有區(qū)別,就不浪費時間貼代碼了。
如果你使用 Spring Boot,可以直接使用 spring-boot-starter-data-jdbc 完成配置:
- spring-boot-starter-data-jdbc
不過需要注意的是,Spring Data JDBC 的邏輯:
- 如果聚合根是一個新的對象,Spring Data JDBC 會遞歸保存所有的關聯(lián)對象。
- 如果聚合根是一個舊的對象,Spring Data JDBC 會刪除除了聚合根之外舊的對象再插入,聚合根會被更新。因為沒有之前對象的狀態(tài),這是一種不得不做的事情。也可以按照自己策略覆蓋相關方法。
使用 Domain Service 變通處理
正是因為和 ORM 一起時候會有各種限制,而抽象一個 Repository 層會帶來大的成本,所以有一種變通的方法。
這種方法不使用充血模型、也不讓 Repository 來保證聚合的一致性,而是使用領域服務來實現(xiàn)相關邏輯,但會被批評為 DDD lite 或不是 “純正的 DDD”。
這種編程范式有如下規(guī)則:
- 按照 DDD 四層模型,Application Service 和 Domain Service 分開,Application Service 負責業(yè)務編排,不是必須的一層,可以由 UI 層兼任。
- 一個聚合使用 DomainService 來保持業(yè)務的一致性,一個聚合只有一個 Domain Service。Domain Service 內使用 ORM 的各種持久化技術。
- 除了 Domain Service 不允許其他地方之間使用 ORM 更新數(shù)據(jù)。
當不被充血模型困住的時候,問題變得更清晰。
DDD 只是手段不是目的,對一般業(yè)務系統(tǒng)而言,充血模型不是必要的,我們的目的是讓編碼和業(yè)務清晰。
這里引入兩個概念:
- 業(yè)務主體。操作領域模型的擬人化對象,用來承載業(yè)務規(guī)則,也就是 Domain Service,比如訂單聚合可以由一個服務來管理,保證業(yè)務的一致性。我們可以命名為:OrderManager.
- 業(yè)務客體。聚合和領域對象,用來承載業(yè)務屬性和數(shù)據(jù)。這些對象需要有狀態(tài)和自己的生命周期,比如 Order、OrderItem。
回歸到原始的編程哲學:
程序 = 數(shù)據(jù)結構 + 算法
業(yè)務主體負責業(yè)務規(guī)則(算法),業(yè)務客體負責業(yè)務屬性和數(shù)據(jù)(數(shù)據(jù)結構),那么用不用 DDD 都能讓代碼清晰、明白和容易處理了。
【本文是51CTO專欄作者“ThoughtWorks”的原創(chuàng)稿件,微信公眾號:思特沃克,轉載請聯(lián)系原作者】