使用Spring Data JDBC实现DDD聚合的示例代码

本文讨论了Spring Data JDBC如何实现DDD中聚合根存储的设计思路,其中主要讨论了是不是每个实体都需要一个对应数据表,这种问题需要根据具体情况而定。

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);
}

如果我们需要一个语法上相同但语义上不同的关系呢?上述订单中包含订单条目OrderItem , 当订单删除时,包含的OrderItem 都删除了,但是看看看看下面案例,也是使用包含一个集合:

class Book {
 // …
 Set<Author> authors = new HashSet<>();
}

当书籍绝版时,将Book删除。所有的作者Author也都丢失了。这当然不是你想要的,因为一些作者可能也写过其他书。

怎么办?

让我们看一看存储库实际存在的内容。这与一遍又一遍的问题密切相关:是否应该在JPA中为每个表创建一个存储库?

而正确和权威的答案是“不”。存储库持久聚合并加载聚合。聚合是一个包含各种对象的群,它应始终保持一致。此外,它应始终保持(和加载)在一起。它有一个对象,称为聚合根,它是唯一允许外部访问或引用聚合内部的代理或管理者。聚合根是传递给存储库的,以便持久化聚合里面的对象群。

这提出了一个问题:Spring Data JDBC如何确定什么是聚合的一部分,哪些不是?答案非常简单:非瞬态non-transient 引用都是聚合的一部分,这样就可从聚合根到达聚合内部所有内容。

OrderItem实例是聚合的一部分,因此被删除; Author正好相反,实例不是Book聚合的一部分,因此不应删除。所以不应该从Book内部去引用那些作者Author对象。

问题解决了。好吧,......不是真的。我们仍然需要存储和访问有关Book和Author之间的关系信息。答案可以在领域驱动设计(DDD)中找到,它建议使用ID而不是直接引用。这适用于各种多对X关系。

如果多个聚合引用同一个实体,则该实体不能成为引用它的多个聚合的一部分,因为它只能是其中一个聚合的一部分。因此,任何“多对一”和“多对多”关系都只能通过引用id来建模实现了。

这样可以实现多种目的:

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

2. 还完全解耦(至少在应用程序的领域模型中)所涉及的两个聚合。

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

a. 以通常的方式保留数据库,包括所有外键。这意味着必须确保以正确的顺序创建和保留聚合。

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

c. 完全删除外键,实现真正的最终一致性。

d. 将引用的聚合保留在不同的数据库中,甚至可能是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.authorId = author.id;
  return authorRef;
 }
}

@Table("Book_Author")
class AuthorRef {
 Long authorId ;
}

class Author {
 @Id Long id;
 String name;
}

请注意额外的类:AuthorRef,它表示有关某个作者的Book聚合的知识。它可能包含有关作者的其他聚合信息,然后实际上会在数据库中重复。考虑到Author数据库可能与Book数据库完全不同,这会产生很多问题。

另请注意,authors是Book 私有字段,AuthorRef实例化在私有方法createAuthorRef中发生。因此聚合之外的任何内容都不能直接访问它。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表示。此外,如果需要反向导航,请在存储库中使用查询方法。这样能清楚确定哪个聚合负责维护引用。

banq注:是不是每个实体都需要一个对应数据表?根据具体情况,Order和OrderItem之间生命周期是一致的,删除订单,订单条目也没有存在意义;而Book和Author则不是生命周期一致的,Book可能是当前有界上下文的聚合根,而Author是另外一个有界上下文如作者管理系统的聚合根,如果删除Book同时,也将Author删除,其实是不符合要求的,这时候应该将Author作为值对象看待,Author的Id就是一个值,然后建立一个类AuthorRef ,包含这个值,作为被Book引用的对象,这样就不是整个Author实体聚合对象被Book引用了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Spring Data JDBC介绍及实现代码

    Spring新增了一个新的数据模块:Spring Data JDBC.Spring Data JDBC背后的想法是提供对关系数据库的访问,而无需处理JPA的复杂性.JPA提供延迟加载,缓存和脏跟踪等功能.果你需要这些功能会很很棒,但会让猜测JPA的行为比非JPA更难. 延迟加载可能会在你不需要时触发昂贵的语句,或者它可能会因异常而失败.当你想要比较一个实体的两个版本是哪个变成脏数据时,缓存可能会妨碍你,让你很难找到所有持久性操作都通过的那个点. Spring Data JDBC目标是实现更简单的

  • 使用Spring Data JDBC实现DDD聚合的示例代码

    本文讨论了Spring Data JDBC如何实现DDD中聚合根存储的设计思路,其中主要讨论了是不是每个实体都需要一个对应数据表,这种问题需要根据具体情况而定. Spring Data JDBC比JPA更容易理解,比如对象引用特性会很有趣.作为第一个示例,请考虑以下领域模型: class PurchaseOrder { private @Id Long id; private String shippingAddress; private Set<OrderItem> items = new

  • 基于Spring Data的AuditorAware审计功能的示例代码

    Spring Data提供支持审计功能:即由谁在什么时候创建或修改实体.Spring Data提供了在实体类的属性上增加@CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate注解,并配置相应的配置项,即可实现审计功能,有系统自动记录 createdBy CreatedDate lastModifiedBy lastModifiedDate 四个属性的值,下面为具体的配置项. 示例 创建一个实体类 package com.hfcsbc.i

  • SpringBoot+Spring Data JPA整合H2数据库的示例代码

    目录 前言 Maven依赖 Conroller 实体类 Repository 数据库脚本文件 配置文件 启动项目 访问H2数据库 查看全部数据 H2数据库文件 运行方式 前言 H2数据库是一个开源的关系型数据库.H2采用java语言编写,不受平台的限制,同时支持网络版和嵌入式版本,有比较好的兼容性,支持相当标准的sql标准 提供JDBC.ODBC访问接口,提供了非常友好的基于web的数据库管理界面 官网:http://www.h2database.com/ Maven依赖 <!--jpa-->

  • Spring Data Exists查询最佳方法编写示例

    目录 简介 领域模型 如何不使用Spring Data来写Exists查询? 用findBy查询模拟存在 使用实例查询来检查存在性 如何使用Spring Data编写Exists查询 用existsBy查询方法检查存在性 用COUNT SQL查询来检查存在性 用CASE WHEN EXISTS SQL查询来检查存在性 结论 简介 在这篇文章中,我将向你展示编写Spring Data Exists查询的最佳方法,从SQL的角度来看,它是高效的. 在做咨询的时候,我遇到了几个常用的选项,而开发者却不

  • Spring Boot 实现Restful webservice服务端示例代码

    1.Spring Boot configurations application.yml spring: profiles: active: dev mvc: favicon: enabled: false datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/wit_neptune?createDatabaseIfNotExist=true&useUnicode=true&

  • Spring BeanUtils忽略空值拷贝的方法示例代码

    目录 简介 获取null属性名(工具类) 示例 工具类 Entity Controller 测试 其他文件 其他网址 简介 说明 本文用示例介绍Spring(SpringBoot)如何使用BeanUtils拷贝对象属性(忽略空值). BeanUtils类所在的包 有两个包都提供了BeanUtils类: Spring的(推荐):org.springframework.beans.BeanUtilsApache的:org.apache.commons.beanutils.BeanUtils 忽略nu

  • Spring Boot加密配置文件特殊内容的示例代码详解

    有时安全不得不考虑,看看新闻泄漏风波事件就知道了我们在用Spring boot进行开发时,经常要配置很多外置参数ftp.数据库连接信息.支付信息等敏感隐私信息,如下 ​ 这不太好,特别是互联网应用,应该用加密的方式比较安全,有点类似一些应用如电商.公安.安检平台.滚动式大屏中奖信息等显示身份证号和手机号都是前几位4109128*********和158*******.那就把图中的明文改造下1. 引入加密包,可选,要是自己实现加解密算法,就不需要引入第三方加解密库 <dependency> &l

  • Java spring boot 实现支付宝支付功能的示例代码

    一.准备工作: 1.登陆支付宝开发者中心,申请一个开发者账号. 地址:https://openhome.alipay.com/ 2.进入研发服务: 3.点击链接进入工具下载页面: 4.点击下载对应版本的RSA公钥生成器: 5.生成公钥密钥(记录你的应用私钥): 6.在支付宝配置公钥(点击保存): 二.搭建demo 1.引入jia包: <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alip

  • Spring Data JPA实现分页Pageable的实例代码

    在JPA中提供了很方便的分页功能,那就是Pageable(org.springframework.data.domain.Pageable)以及它的实现类PageRequest(org.springframework.data.domain.PageRequest),详细的可以见示例代码. 1.改变CustomerRepository方法​ /** * 一个参数,匹配两个字段 * @param name2 * @Param pageable 分页参数 * @return * 这里Param的值和

随机推荐