Spring Data JDBC、引用和聚合

工程 | Jens Schauder | 2018年9月24日 | ...

在我之前的博客文章中,我描述了如何设置和使用 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 类中引用。

问题解决了。 好吧,… 并非真的。 我们仍然需要存储和访问有关 BookAuthor 之间关系的信息。 答案可以再次在领域驱动设计 (DDD) 中找到,它建议使用 ID 而不是直接引用。 这适用于所有类型的多对x关系。

如果多个聚合引用同一个实体,则该实体不能成为引用它的那些聚合的一部分,因为它只能是一个聚合的一部分。 因此,任何多对一和多对多关系都必须仅通过引用 ID 来建模。

如果您应用此方法,您将实现多项事情

  1. 您清楚地表示聚合的边界。

  2. 您还将完全分离(至少在应用程序的域模型中)所涉及的两个聚合。

  3. 这种分离可以用不同的方式在数据库中表示

    1. 保持数据库通常的样子,包括所有外键。 这意味着您必须确保以正确的顺序创建和持久化聚合。

    2. 使用延迟约束,该约束仅在事务的提交阶段进行检查。 这可能会实现更高的吞吐量。 它还编纂了最终一致性的一个版本,其中“最终”与事务的结束相关联。 这也允许引用从未存在的聚合,只要它仅在事务期间发生即可。 这可能有助于避免大量的底层代码,仅仅为了满足外键和非空约束。

    3. 完全删除外键,允许真正的最终一致性。

    4. 将引用的聚合持久化到不同的数据库中,甚至可能是 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
)

获取 Spring 新闻简报

通过 Spring 新闻简报保持联系

订阅

更进一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获得支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将到来的活动

查看 Spring 社区中所有即将发生的事件。

查看全部