使用Spring Data JDBC实现DDD聚合的示例代码
作者:解道 发布时间:2022-05-04 05:11:23
本文讨论了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引用了。
来源:https://www.jdon.com/50192


猜你喜欢
- 上一篇实现了移动端微信消息界面功能,以此为基础继续完善服务端功能服务端微信消息页实现微信消息界面的实现,和登录,注册是类似的,无非就是接受客
- 本文导读中秋节是中国民间的传统节日,中秋节源自天象崇拜由上古时代秋夕祭月演变而来。中秋节自古便有祭月、赏月、吃月饼等民俗,流传至今,经久不息
- 最近在做上传文件的服务,简单看了网上的教程。结合实践共享出代码。由于网上的大多数没有服务端的代码,这可不行呀,没服务端怎么调试呢。Ok,先上
- 一、why(为什么要用Hibernate缓存?)Hibernate是一个持久层框架,经常访问物理数据库。为了降低应用程序对物理数据源访问的频
- c#开发cad如何预览图块1.定义变量的方法代码如下2. 获取GetDwgImag图像的方法代码3.实现显示DWG文件的方法代码方
- 对于开发游戏项目的同胞来说,Timer 这个东西肯定不会陌生,今天对以前自己经常使用的定时进行了一番小小的总结!没有写具体实现的原理,只是列
- 在用HTML5做跨平台应用开发时,尝尝会用到java和js方法互调的问题,对初学者而言,可能会有点难,在这里分享一些自己在实际开发过程中的用
- 详解Android Webview加载网页时发送HTTP头信息当你点击一个超链接进行跳转时,WebView会自动将当前地址作为Referer
- Android自定义view是什么在我们的日常开发中,很多时候系统提供的view是无法满足我们的需求的,例如,我们想给一个edittext加
- 网上很多资料在描述Java内存模型的时候,都会介绍有一个主存,然后每个工作线程有自己的工作内存。数据在主存中会有一份,在工作内存中也有一份。
- 返回集合为null还是空集合及空集合的三种写法个人认为在自己写接口时,需要返回集合时返回一个空集合,比如mybatis查询如果返回一个集合,
- spring security中遇到的问题1.An Authentication object was not found in the S
- Maven,是一个Java开发比较常用的项目管理工具,可以对 Java 项目进行构建、依赖管理。对于很多Java程序员来说,分层架构都是不陌
- 1. 问题描述springboot的面世,成为Java开发者的一大福音,大大提升了开发的效率,其实springboot只是在maven的基础
- 引导语上一小节我们学习了 Socket,本文我们来看看服务端套接字 API:ServerSocket,本文学习完毕之后,我们就可以把客户端
- 今天是圣诞节平安夜,为此特别制作了一个雪花飘落的场景,我们的雪花渲染方式不同于网上流行的使用Camera Filter,需要将脚本挂接到相机
- 消息过滤RocketMQ分布式消息队列的消息过滤方式有别于其它MQ中间件,是在Consumer端订阅消息时再做消息过滤的。RocketMQ这
- 本问介绍了Collections工具类两种sort()方法,具体如下:一、Collections工具类两种sort()方法格式一: publ
- 微信支付流程都是我自己工作中开发的,亲测可用,不喜勿喷。controller中我是这么写的,你们需要根据自己的业务需求改动。Response
- 导入redis的jar包<!-- redis --> <dependency>