搶先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多在我之前的部落格文章中,我描述了如何設定和使用 Spring Data JDBC。 我也描述了使 Spring Data JDBC 比 JPA 更容易理解的前提。 一旦您考慮到參考,這就變得很有趣了。 作為第一個範例,請考慮以下網域模型
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;
}
此外,考慮一個定義如下的儲存庫
interface OrderRepository extends CrudRepository<PurchaseOrder, Long> {
@Query("select count(*) from order_item")
int countItems();
}
如果您建立一個包含項目的訂單,您可能希望所有項目都得到持久化。 而這正是發生的事
@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
,其所有項目也應被刪除。 再一次,這就是它應有的方式。
…
repository.delete(saved);
assertThat(repository.count()).isEqualTo(0);
assertThat(repository.countItems()).isEqualTo(0);
}
但是,如果我們考慮一個語法相同但語義不同的關係呢?
class Book {
// …
Set<Author> authors = new HashSet<>();
}
當一本書絕版時,您會刪除它。 然後所有的作者都不見了。 這肯定不是您想要的,因為有些作者可能還寫了其他書。 現在,這沒有道理。 還是有呢? 我認為有。
為了理解為什麼這有道理,我們需要退一步,看看儲存庫實際持久化了什麼。 這與一個反覆出現的問題密切相關:您是否應該在 JPA 中為每個表建立一個儲存庫?
而正確且權威的答案是「否」。 儲存庫持久化和載入聚合。 聚合是形成一個單位的物件群組,它應該始終保持一致。 此外,它應該始終一起持久化(和載入)。 它有一個單一物件,稱為聚合根,它是唯一允許觸摸或參考聚合內部的東西。 聚合根是傳遞到儲存庫以持久化聚合的物件。
這引出了一個問題:Spring Data JDBC 如何確定哪些是聚合的一部分,哪些不是? 答案非常簡單:您可以從聚合根開始,通過非暫時性參考到達的所有內容都是聚合的一部分。
考慮到這一點,OrderRepository
的行為就完全合理了。 OrderItem
實例是聚合的一部分,因此會被刪除。 相反,Author
實例不是 Book
聚合的一部分,因此不應被刪除。 所以他們應該只是不從 Book
類別中被參考。
問題解決了。 好吧,... 並非真的如此。 我們仍然需要儲存和存取有關 Book
和 Author
之間關係的資訊。 答案再次可以在領域驅動設計 (DDD) 中找到,它建議使用 ID 而不是直接參考。 這適用於所有種類的多對 X 關係。
如果多個聚合參考同一個實體,則該實體不能成為參考它的那些聚合的一部分,因為它只能成為恰好一個聚合的一部分。 因此,任何多對一和多對多關係都必須通過僅參考 ID 來建模。
如果您應用此方法,您將實現多件事
您清楚地表示了聚合的邊界。
您還可以完全解耦(至少在應用程式的網域模型中)所涉及的兩個聚合。
這種分離可以用不同的方式在資料庫中表示
保持資料庫的正常狀態,包括所有外鍵。 這意味著您必須確保以正確的順序建立和持久化聚合。
使用延遲約束,該約束僅在事務的提交階段進行檢查。 這可能會實現更高的吞吐量。 它還編纂了一種最終一致性的版本,其中「最終」與事務結束相關聯。 這也允許參考從不存在的聚合,只要它僅在事務期間發生。 這對於避免大量的基礎架構程式碼以滿足外鍵和非空約束可能很有用。
完全刪除外鍵,允許真正的最終一致性。
在不同的資料庫中持久化參考的聚合,甚至可能是 No SQL 儲存。
無論您採取多麼大的分離程度,即使是 Spring Data JDBC 強制執行的最小分離,也會鼓勵應用程式的模組化。 此外,如果您嘗試遷移一個真正的單體式 10 年歷史的應用程式,您會明白這有多麼有價值。
使用 Spring Data JDBC,您可以像這樣對多對多關係進行建模
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.author = author.id;
return authorRef;
}
}
@Table("Book_Author")
class AuthorRef {
Long author;
}
class Author {
@Id Long id;
String name;
}
請注意額外的類別 (AuthorRef
),它表示 Book 聚合關於作者的知識。 它可能包含關於作者的其他聚合資訊,然後實際上會在資料庫中複製。 考慮到作者資料庫可能與書籍資料庫完全不同,這使得很多事情變得有意義。
另請注意,作者集合是一個私有欄位,並且 AuthorRef
實例的實例化發生在一個私有方法中。 因此,聚合之外的任何東西都無法直接存取它。 Spring Data JDBC 並不要求這樣做,但 DDD 鼓勵這樣做。 網域的使用方式如下
@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);
}
總之:Spring Data JDBC 不支援多對一或多對多關係。 為了對這些關係進行建模,請使用 ID。 這鼓勵了網域模型的乾淨模組化。 它還消除了一整類問題,如果這種映射是可能的話,人們必須解決並學會推理。
通過類似的思路,避免雙向依賴。 聚合內的參考從聚合根指向元素。 聚合之間的參考通過一個方向上的 ID 來表示。 此外,如果您需要導航反向,請在儲存庫中使用查詢方法。 這使得哪個聚合負責維護參考變得一目瞭然。
以下是範例使用的資料庫結構。
Purchase_Order (
id
shipping_address
)
Order_Item (
purchase_order
quantity
product
);
Book (
id
title
)
Author (
id
name
)
Book_Author (
book
author
)