使用Spring Data JDBC實(shí)現(xiàn)DDD聚合的示例代碼
本文討論了Spring Data JDBC如何實(shí)現(xiàn)DDD中聚合根存儲(chǔ)的設(shè)計(jì)思路,其中主要討論了是不是每個(gè)實(shí)體都需要一個(gè)對(duì)應(yīng)數(shù)據(jù)表,這種問題需要根據(jù)具體情況而定。
Spring Data JDBC比JPA更容易理解,比如對(duì)象引用特性會(huì)很有趣。作為第一個(gè)示例,請(qǐng)考慮以下領(lǐng)域模型:
class PurchaseOrder { private @Id Long id; private String shippingAddress; private Set<OrderItem> items = new HashSet<>(); void addItem(int quantity, String product) { items.add(createOrderItem(quantity, product)); } private OrderItem createOrderItem(int quantity, String product) { OrderItem item = new OrderItem(); item.product = product; item.quantity = quantity; return item; } } class OrderItem { int quantity; String product; }
另外,考慮如下定義的存儲(chǔ)庫(kù):
interface OrderRepository extends CrudRepository<PurchaseOrder, Long> { @Query("select count(*) from order_item") int countItems(); }
如果使用商品創(chuàng)建訂單,希望所有商品都能保存:
@Autowired OrderRepository repository; @Test public void createUpdateDeleteOrder() { PurchaseOrder order = new PurchaseOrder(); order.addItem(4, "Captain Future Comet Lego set"); order.addItem(2, "Cute blue angler fish plush toy"); PurchaseOrder saved = repository.save(order); assertThat(repository.count()).isEqualTo(1); assertThat(repository.countItems()).isEqualTo(2); …
此外,如果刪除PurchaseOrder,它的所有項(xiàng)目也應(yīng)該被刪除。
… repository.delete(saved); assertThat(repository.count()).isEqualTo(0); assertThat(repository.countItems()).isEqualTo(0); }
如果我們需要一個(gè)語(yǔ)法上相同但語(yǔ)義上不同的關(guān)系呢?上述訂單中包含訂單條目OrderItem , 當(dāng)訂單刪除時(shí),包含的OrderItem 都刪除了,但是看看看看下面案例,也是使用包含一個(gè)集合:
class Book { // … Set<Author> authors = new HashSet<>(); }
當(dāng)書籍絕版時(shí),將Book刪除。所有的作者Author也都丟失了。這當(dāng)然不是你想要的,因?yàn)橐恍┳髡呖赡芤矊戇^其他書。
怎么辦?
讓我們看一看存儲(chǔ)庫(kù)實(shí)際存在的內(nèi)容。這與一遍又一遍的問題密切相關(guān):是否應(yīng)該在JPA中為每個(gè)表創(chuàng)建一個(gè)存儲(chǔ)庫(kù)?
而正確和權(quán)威的答案是“不”。存儲(chǔ)庫(kù)持久聚合并加載聚合。聚合是一個(gè)包含各種對(duì)象的群,它應(yīng)始終保持一致。此外,它應(yīng)始終保持(和加載)在一起。它有一個(gè)對(duì)象,稱為聚合根,它是唯一允許外部訪問或引用聚合內(nèi)部的代理或管理者。聚合根是傳遞給存儲(chǔ)庫(kù)的,以便持久化聚合里面的對(duì)象群。
這提出了一個(gè)問題:Spring Data JDBC如何確定什么是聚合的一部分,哪些不是?答案非常簡(jiǎn)單:非瞬態(tài)non-transient 引用都是聚合的一部分,這樣就可從聚合根到達(dá)聚合內(nèi)部所有內(nèi)容。
OrderItem實(shí)例是聚合的一部分,因此被刪除; Author正好相反,實(shí)例不是Book聚合的一部分,因此不應(yīng)刪除。所以不應(yīng)該從Book內(nèi)部去引用那些作者Author對(duì)象。
問題解決了。好吧,......不是真的。我們?nèi)匀恍枰鎯?chǔ)和訪問有關(guān)Book和Author之間的關(guān)系信息。答案可以在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)中找到,它建議使用ID而不是直接引用。這適用于各種多對(duì)X關(guān)系。
如果多個(gè)聚合引用同一個(gè)實(shí)體,則該實(shí)體不能成為引用它的多個(gè)聚合的一部分,因?yàn)樗荒苁瞧渲幸粋€(gè)聚合的一部分。因此,任何“多對(duì)一”和“多對(duì)多”關(guān)系都只能通過引用id來建模實(shí)現(xiàn)了。
這樣可以實(shí)現(xiàn)多種目的:
1. 清楚地表示了聚合的邊界。
2. 還完全解耦(至少在應(yīng)用程序的領(lǐng)域模型中)所涉及的兩個(gè)聚合。
3. 這種分離可以用不同的方式在數(shù)據(jù)庫(kù)中表示:
a. 以通常的方式保留數(shù)據(jù)庫(kù),包括所有外鍵。這意味著必須確保以正確的順序創(chuàng)建和保留聚合。
b. 使用延遲約束,僅在事務(wù)的提交階段進(jìn)行檢查。這可能會(huì)提高吞吐量。它還編纂了最終一致性的版本,其中“最終”與交易結(jié)束相關(guān)聯(lián)。這也允許引用從未存在的聚合,只要它僅在事務(wù)期間發(fā)生。這對(duì)于避免大量基礎(chǔ)結(jié)構(gòu)代碼只是為了滿足外鍵和非空約束可能是有用的。
c. 完全刪除外鍵,實(shí)現(xiàn)真正的最終一致性。
d. 將引用的聚合保留在不同的數(shù)據(jù)庫(kù)中,甚至可能是No SQL存儲(chǔ)。
無(wú)論如何,即使Spring Data JDBC也鼓勵(lì)應(yīng)用模塊化。此外,如果嘗試遷移一個(gè)具有10年歷史的單體,你就會(huì)明白它的價(jià)值。
使用Spring Data JDBC,您可以建模多對(duì)多關(guān)系,如下所示:
class Book { private @Id Long id; private String title; private Set<AuthorRef> authors = new HashSet<>(); public void addAuthor(Author author) { authors.add(createAuthorRef(author)); } private AuthorRef createAuthorRef(Author author) { Assert.notNull(author, "Author must not be null"); Assert.notNull(author.id, "Author id, must not be null"); AuthorRef authorRef = new AuthorRef(); authorRef.authorId = author.id; return authorRef; } } @Table("Book_Author") class AuthorRef { Long authorId ; } class Author { @Id Long id; String name; }
請(qǐng)注意額外的類:AuthorRef,它表示有關(guān)某個(gè)作者的Book聚合的知識(shí)。它可能包含有關(guān)作者的其他聚合信息,然后實(shí)際上會(huì)在數(shù)據(jù)庫(kù)中重復(fù)??紤]到Author數(shù)據(jù)庫(kù)可能與Book數(shù)據(jù)庫(kù)完全不同,這會(huì)產(chǎn)生很多問題。
另請(qǐng)注意,authors是Book 私有字段,AuthorRef實(shí)例化在私有方法createAuthorRef中發(fā)生。因此聚合之外的任何內(nèi)容都不能直接訪問它。Spring Data JDBC絕不需要這樣做,但DDD鼓勵(lì)這么做。
下面是測(cè)試:
@Test public void booksAndAuthors() { Author author = new Author(); author.name = "Greg L. Turnquist"; author = authors.save(author); Book book = new Book(); book.title = "Spring Boot"; book.addAuthor(author); books.save(book); books.deleteAll(); assertThat(authors.count()).isEqualTo(1); }
上述完成了我們?cè)O(shè)想功能:刪除書籍后,并沒有將書籍作者數(shù)據(jù)表數(shù)據(jù)全部刪除,雖然作者是書籍的一個(gè)私有字段。
總結(jié)一下:
Spring Data JDBC不支持多對(duì)一或多對(duì)多關(guān)系。要對(duì)這些進(jìn)行建模,請(qǐng)使用ID。
這鼓勵(lì)了領(lǐng)域模型的清晰模塊化。
通過類似的思路,避免雙向依賴。聚合內(nèi)部的引用是從聚合根到元素。聚合之間的引用使用只在一個(gè)關(guān)聯(lián)方向上使用ID表示。此外,如果需要反向?qū)Ш?,?qǐng)?jiān)诖鎯?chǔ)庫(kù)中使用查詢方法。這樣能清楚確定哪個(gè)聚合負(fù)責(zé)維護(hù)引用。
banq注:是不是每個(gè)實(shí)體都需要一個(gè)對(duì)應(yīng)數(shù)據(jù)表?根據(jù)具體情況,Order和OrderItem之間生命周期是一致的,刪除訂單,訂單條目也沒有存在意義;而Book和Author則不是生命周期一致的,Book可能是當(dāng)前有界上下文的聚合根,而Author是另外一個(gè)有界上下文如作者管理系統(tǒng)的聚合根,如果刪除Book同時(shí),也將Author刪除,其實(shí)是不符合要求的,這時(shí)候應(yīng)該將Author作為值對(duì)象看待,Author的Id就是一個(gè)值,然后建立一個(gè)類AuthorRef ,包含這個(gè)值,作為被Book引用的對(duì)象,這樣就不是整個(gè)Author實(shí)體聚合對(duì)象被Book引用了。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
spring?boot教程之建立第一個(gè)HelloWorld
這篇文章主要介紹了spring?boot教程之建立第一個(gè)HelloWorld的相關(guān)資料,需要的朋友可以參考下2022-08-08mybatis的坑-integer類型為0的數(shù)據(jù)if?test失效問題
這篇文章主要介紹了mybatis的坑-integer類型為0的數(shù)據(jù)if?test失效問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01使用Java獲取List交集數(shù)據(jù)的實(shí)現(xiàn)方案小結(jié)
今天遇到一個(gè)小需求,當(dāng)用戶上傳了一個(gè)關(guān)于用戶數(shù)據(jù)的列表,我們需要將其與數(shù)據(jù)庫(kù)中已有的用戶數(shù)據(jù)進(jìn)行比較,所以本文給大家介紹了使用Java獲取List交集數(shù)據(jù)的實(shí)現(xiàn)方案小結(jié),文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-03-03Java中Vector、ArrayList、LinkedList的關(guān)系詳細(xì)解析
這篇文章主要介紹了Java中Vector、ArrayList、LinkedList的關(guān)系詳細(xì)解析,Vector和ArrayList底層均為Object數(shù)組,LinkedList底層是Node節(jié)點(diǎn),Vector是線程安全(底層方法均添加synchronized),ArrayList、LinkedList是線程不安全(無(wú)鎖),需要的朋友可以參考下2023-11-11Java獲取一維數(shù)組的最小值實(shí)現(xiàn)方法
這篇文章主要介紹了Java獲取一維數(shù)組的最小值實(shí)現(xiàn)方法,需要的朋友可以參考下2014-02-02SpringBoot父子線程數(shù)據(jù)傳遞的五種方案介紹
在實(shí)際開發(fā)過程中我們需要父子之間傳遞一些數(shù)據(jù),比如用戶信息等。該文章從5種解決方案解決父子之間數(shù)據(jù)傳遞困擾,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-09-09Java運(yùn)行時(shí)數(shù)據(jù)區(qū)概述詳解
這篇文章主要介紹了Java運(yùn)行時(shí)數(shù)據(jù)區(qū)概述,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03SpringBoot3.0集成MybatisPlus的實(shí)現(xiàn)方法
本文主要介紹了SpringBoot3.0集成MybatisPlus的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08