更進一步
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
)