領先一步
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 來建模。
如果你應用這個,你會實現多重效果:
你清楚地標示了聚合的邊界。
你還在應用程式的領域模型中完全解耦了所涉及的兩個聚合。
這種分離可以在資料庫中以不同的方式表示:
保持資料庫的正常狀態,包括所有外部索引鍵。這意味著你必須確保以正確的順序建立和持久化聚合。
使用延遲約束,這些約束僅在事務的提交階段才會被檢查。這可能會實現更高的吞吐量。它還編碼了一種最終一致性版本,其中“最終”與事務的結束繫結。這也允許引用從未存在的聚合,只要它只發生在事務期間。這對於避免大量基礎設施程式碼以僅僅滿足外部索引鍵和非空約束可能很有用。
完全刪除外部索引鍵,實現真正的最終一致性。
將引用的聚合持久化到不同的資料庫中,甚至可能是 NoSQL 儲存。
無論你將分離推到多遠,即使是 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
)