天天看點

持久化DDD聚合

在本教程中,我們将探索使用不同技術持久化DDD 聚合的可能性。

聚合是一組始終需要保持一緻的業務對象。是以,我們在事務中作為一個整體儲存和更新聚合。

聚合是DDD中的一個重要戰術模式,它有助于保持業務對象的一緻性。然而,聚合的概念在DDD上下文之外也很有用。

在許多業務案例中,這種模式都可以派上用場。根據經驗,當同一個事務中有多個對象被更改時,我們應該考慮使用聚合。

讓我們看看在為訂單購買模組化時如何應用這一點。

是以,讓我們假設我們想要模組化一個采購訂單:

<code>class Order {</code>

<code>   private Collection&lt;OrderLine&gt; orderLines;</code>

<code>   private Money totalCost;</code>

<code>   // ...</code>

<code>}</code>

<code>class OrderLine {</code>

<code>   private Product product;</code>

<code>   private int quantity;</code>

<code>class Product {</code>

<code>   private Money price;</code>

這些類形成一個簡單的聚合。訂單的orderLines和totalCost字段必須始終保持一緻,即totalCost的值應該總是等于所有orderLines的總和。

現在,我們可能都想把所有這些都變成成熟的Java bean。但是,請注意,按照順序引入簡單的getter和setter很容易打破模型的封裝,并違反業務限制。

讓我們看看會出什麼問題。

讓我們想象一下,如果我們決定向Order類中的所有屬性(包括setOrderTotal)添加getter和setter,會發生什麼。

沒有什麼可以阻止我們執行以下代碼:

<code>Order order = new Order();</code>

<code>order.setOrderLines(Arrays.asList(orderLine0, orderLine1));</code>

<code>order.setTotalCost(Money.zero(CurrencyUnit.USD)); // this doesn't look good...</code>

在這段代碼中,我們手動将 totalCost 屬性設定為零,這違反了一條重要的業務規則。當然,總成本不應該是零美元!

我們需要一種方法來保護我們的業務規則。讓我們看看聚合根是如何起作用的。

聚合根是一個作為聚合入口點的類。所有業務操作都應該通過根。這樣,聚合根就可以保證聚合保持一緻的狀态。

它的根本是考慮所有業務不變量。

在我們的示例中, Order 類是聚合根的正确候選對象。我們隻需要做一些修改,以確定聚合始終一緻:

<code>   private final List&lt;OrderLine&gt; orderLines;</code>

<code>   Order(List&lt;OrderLine&gt; orderLines) {</code>

<code>       checkNotNull(orderLines);</code>

<code>       if (orderLines.isEmpty()) {</code>

<code>           throw new IllegalArgumentException("Order must have at least one order line item");</code>

<code>       }</code>

<code>       this.orderLines = new ArrayList&lt;&gt;(orderLines);</code>

<code>       totalCost = calculateTotalCost();</code>

<code>   }</code>

<code>   void addLineItem(OrderLine orderLine) {</code>

<code>       checkNotNull(orderLine);</code>

<code>       orderLines.add(orderLine);</code>

<code>       totalCost = totalCost.plus(orderLine.cost());</code>

<code>   void removeLineItem(int line) {</code>

<code>       OrderLine removedLine = orderLines.remove(line);</code>

<code>       totalCost = totalCost.minus(removedLine.cost());</code>

<code>   Money totalCost() {</code>

<code>       return totalCost;</code>

使用聚合根現在允許我們更容易地将Product 和OrderLine轉換為不可變對象,其中所有屬性都是final的。

我們可以看到,這是一個非常簡單的集合。

我們可以簡單地計算出每次的總成本而不用使用字段。

但是,現在我們隻讨論聚合持久性,而不是聚合設計。請繼續關注,因為這個特定領域很快就會派上用場。

這在持久性技術中發揮了多大的作用?讓我們來看看。最終,這将幫助我們為下一個項目選擇正确的持久性工具。

在本節中,讓我們嘗試使用JPA和Hibernate持久化訂單聚合。我們将使用Spring Boot和JPA starter:

<code>&lt;dependency&gt;</code>

<code>   &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;</code>

<code>   &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;</code>

<code>&lt;/dependency&gt;</code>

對我們大多數人來說,這似乎是最自然的選擇。畢竟,我們花了多年的時間研究關系系統,我們都知道流行的ORM架構。

在使用ORM架構時,最大的問題可能是模型設計的簡化。有時也被稱為 對象關系阻抗失配。讓我們想想,如果我們想保持我們的訂單總量:

<code>@DisplayName("given order with two line items, when persist, then order is saved")</code>

<code>@Test</code>

<code>public void test() throws Exception {</code>

<code>   // given</code>

<code>   JpaOrder order = prepareTestOrderWithTwoLineItems();</code>

<code>   // when</code>

<code>   JpaOrder savedOrder = repository.save(order);</code>

<code>   // then</code>

<code>   JpaOrder foundOrder = repository.findById(savedOrder.getId())</code>

<code>     .get();</code>

<code>   assertThat(foundOrder.getOrderLines()).hasSize(2);</code>

此時,該測試将抛出一個異常:java.lang.IllegalArgumentException: Unknown entity: com.baeldung.ddd.order.Order。顯然,我們遺漏了一些JPA需求:

1、添加映射注釋

2、OrderLine和Product類必須是實體或@Embeddable類,而不是簡單的值對象

3、為每個實體@Embeddable類添加一個空的構造函數

4、用簡單類型替換貨币屬性

嗯,我們需要修改Order aggregate的設計以便能夠使用JPA。雖然添加注釋不是什麼大問題,但是其他需求可能會帶來很多問題。

嘗試将一個聚合體放入JPA的第一個問題是,我們需要打破我們的value對象的設計:它們的屬性不再是final,我們需要打破封裝。

我們需要在OrderLine和 Product中添加人工ids,即使這些類從未被設計為具有辨別符。我們希望它們是簡單的值對象。

可以使用 @Embedded 和@ElementCollection注解,但這種方法在使用複雜對象圖時可能會使事情變得複雜(例如,@Embeddable對象具有另一個@Embedded屬性等)。

使用@Embedded注解隻是向父表添加平面屬性。除此之外,基本屬性(例如字元串類型)仍然需要setter方法,這違反了預期的值對象設計。

空構造函數要求強制value對象屬性不再是final,這打破了我們最初設計的一個重要方面。說實話,Hibernate可以使用私有的no-args構造函數,這稍微減輕了一些問題,但它還遠遠不夠完美。

即使使用私有預設構造函數,我們也不能将屬性标記為final,或者需要在預設構造函數中使用預設值(通常為空)初始化它們。

然而,如果我們想要完全相容JPA,我們必須至少對預設構造函數使用受保護的可見性,這意味着同一包中的其他類可以在不指定屬性值的情況下建立值對象。

不幸的是,我們不能期望JPA自動将第三方複雜類型映射到表中。看看我們在上一節中介紹了多少變化!

例如,在處理我們的訂單集合時,我們将遇到堅持Joda Money 字段的困難。

在這種情況下,我們可能結束編寫JPA 2.1中可用的自定義類型@Converter 。不過,這可能需要一些額外的工作。

或者,我們也可以将貨币屬性分為兩種基本屬性。例如,貨币機關的字元串和實際值的BigDecimal。

雖然我們可以隐藏實作細節,并且仍然通過公共方法API使用Money類,但實踐表明,大多數開發人員無法證明額外的工作是合理的,而隻是将模型簡化以符合JPA規範。

雖然JPA是世界上采用最多的規範之一,但它可能不是儲存訂單聚合的最佳選擇。

如果我們想要我們的模型反映真實的業務規則,我們應該将它設計成不是底層表的簡單1:1表示。

基本上,我們有三個選擇:

1、建立一組簡單的資料類,并使用它們來持久化和重新建立豐富的業務模型。不幸的是,這可能需要很多額外的工作。

2、接受JPA的限制并選擇合适的折衷方案。

3、考慮另一個技術。

第一種選擇的潛力最大。實際上,大多數項目都是使用第二種方法開發的。

現在,讓我們考慮另一種持久聚合的技術。

文檔存儲是存儲資料的另一種方式。取代使用關系和表,我們儲存整個對象。這使得文檔存儲成為持久化聚合的理想候選對象。

為了滿足本教程的需求,我們将重點介紹json類型的文檔。

讓我們更深入地了解一下在MongoDB這樣的文檔存儲中,訂單持久性問題是如何出現的。

現在,有很多資料庫可以存儲JSON資料,其中最流行的是MongoDB。MongoDB實際上是以二進制形式存儲BSON或JSON。

x幸虧MongoDB,我們可以按原樣存儲訂單示例聚合。

在我們繼續之前,讓我們添加Spring Boot MongoDB啟動器:

<code>   &lt;artifactId&gt;spring-boot-starter-data-mongodb&lt;/artifactId&gt;</code>

現在我們可以運作一個類似于JPA示例的測試用例,但這次使用MongoDB:

<code>@DisplayName("given order with two line items, when persist using mongo repository, then order is saved")</code>

<code>void test() throws Exception {</code>

<code>   Order order = prepareTestOrderWithTwoLineItems();</code>

<code>   repo.save(order);</code>

<code>   List&lt;Order&gt; foundOrders = repo.findAll();</code>

<code>   assertThat(foundOrders).hasSize(1);</code>

<code>   List&lt;OrderLine&gt; foundOrderLines = foundOrders.iterator()</code>

<code>     .next()</code>

<code>     .getOrderLines();</code>

<code>   assertThat(foundOrderLines).hasSize(2);</code>

<code>   assertThat(foundOrderLines).containsOnlyElementsOf(order.getOrderLines());</code>

重要的是,我們沒有改變原始的聚合類的順序;不需要為貨币類建立預設構造函數、設定器或自定義轉換器。

下面是我們在商店裡的訂單總和:

<code>{</code>

<code> "_id": ObjectId("5bd8535c81c04529f54acd14"),</code>

<code> "orderLines": [</code>

<code>   {</code>

<code>     "product": {</code>

<code>       "price": {</code>

<code>         "money": {</code>

<code>           "currency": {</code>

<code>             "code": "USD",</code>

<code>             "numericCode": 840,</code>

<code>             "decimalPlaces": 2</code>

<code>           },</code>

<code>           "amount": "10.00"</code>

<code>         }</code>

<code>     },</code>

<code>     "quantity": 2</code>

<code>   },</code>

<code>           "amount": "5.00"</code>

<code>     "quantity": 10</code>

<code> ],</code>

<code> "totalCost": {</code>

<code>   "money": {</code>

<code>     "currency": {</code>

<code>       "code": "USD",</code>

<code>       "numericCode": 840,</code>

<code>       "decimalPlaces": 2</code>

<code>     "amount": "70.00"</code>

<code> },</code>

<code> "_class": "com.baeldung.ddd.order.mongo.Order"</code>

這個簡單的BSON文檔将整個訂單聚合在一起,與我們最初的概念一緻。

注意,BSON文檔中的複雜對象被簡單地序列化為一組正常JSON屬性。是以,即使是第三方類(比如 Joda Money)也可以輕松序列化,而無需簡化模型。

使用MongoDB持久化聚合比使用JPA更簡單。

這并不意味着MongoDB優于傳統的資料庫。在許多合法的情況下,我們甚至不應該嘗試将我們的類模組化為聚合,而是使用SQL資料庫。

盡管如此,當我們确定了一組對象,這些對象應該根據複雜的需求始終保持一緻時,那麼使用文檔存儲可能是一個非常有吸引力的選擇。

在DDD中,聚合通常包含系統中最複雜的對象。與大多數CRUD應用程式相比,使用它們需要一種非常不同的方法。

使用流行的ORM解決方案可能會導緻過于簡單或過度公開的領域模型,這通常無法表達或強制執行複雜的業務規則。

文檔存儲可以使持久化聚合變得更容易,而不會犧牲模型的複雜性。

所有示例的完整源代碼都可以在GitHub 上找到。

作者:Mike Wojtyna 譯者:康仔