问题描述
我是 Axon 框架、CQRS 和 DDD 的新手。我被教导使用关系数据库创建简单的 CRUD 应用程序。因此,我首先专注于构建数据模型,而不是领域模型。 我想改变我的软件方法,使其更加务实,并创建现实世界的应用程序。因此,我想使用 CQRS 模式和事件溯源。
我现在正在使用 Spring Boot 和 Axon 框架开发一个库应用程序。基本要求之一是用户已借书。我有两个用于 User 和 Book 的聚合。
这是我的 BookAggregate:
@Data
@AllArgsConstructor @NoArgsConstructor
@Aggregate
public class BookAggregate {
@AggregateIdentifier
private UUID id;
private String name;
private String isbnNumber;
private int amountOfcopies;
private Author author;
private Genre genre;
private PublishingHouse publishingHouse;
...
这是我的 UserAggregate:
@Data
@AllArgsConstructor @NoArgsConstructor
@Aggregate
public class UserAggregate {
@AggregateIdentifier
private UUID id;
private String firstName;
private String lastName;
private String email;
private String password;
private String confirmPassword;
private Set<Role> roles;
private Date birthDate;
private String telNumber;
private Address address;
...
我的问题是:我是否应该在这两个聚合之间创建一个中间聚合,例如 sql JOIN?在这个例子中,我想看看在 Axon 框架中实现了两个聚合相互通信的相似用例。
解决方法
我应该在这两个聚合之间创建一个中间聚合吗 像 SQL JOIN 之类的东西?在这个例子中,我想看看如何 两个聚合相互通信的类似用例是 在 Axon 框架中实现。
好问题。我先说不,然后试着帮助你理解为什么。
让我们从聚合存在的原因开始。来自DDD reference:
聚合
在具有复杂关联的模型中,很难保证对象变化的一致性。
[...]
因此: 将实体和值对象聚集成聚合体,并围绕每个定义边界。选择一个实体作为每个聚合的根,并允许外部对象只持有对根的引用
好的,所以使用聚合的原因是为了避免中介。
因为你在做 CQRS(这意味着 DDD 和事件源),你有一个命令端和一个查询端(命令查询请求隔离)。如果您更熟悉 CRUD 风格的应用程序和关系数据库,那么您很可能关注的是查询端而不是命令端。
让我们在 CQRS 中澄清一些事情:
- 聚合仅与命令端相关
- 聚合负责处理命令和发布事件
- 聚合负责执行业务规则
这意味着对于命令端: 命令被传递给聚合,聚合决定是发布事件还是拒绝命令。
在查询端: 处理源自命令端的事件以构建查询模型(可能就是您所说的数据模型)
因此,我首先专注于构建数据模型,而不是领域 型号
在事件源系统中,您通常关注领域模型和相关事件。查询模型可以完全基于领域模型中的事件构建。
由于您还没有关注领域模型,因此您不必关心聚合:)
再次回到你最初的问题:
基本要求之一是用户已借书
我应该在这两个聚合之间创建一个中间聚合吗 类似于 SQL JOIN?
根据您的域,您已经有一个用户和图书聚合。您只有一个要求,即用户可以借书。现在我不是您所在领域的专家,但问问自己:为什么需要另一个聚合?这个新的聚合将负责什么?如果允许用户借书,那么仅让 Book.borrow(userId)
发布 BookBorrowed(bookId,userId)
事件会有什么问题?
我非常非常喜欢@martingreber 之前给出的建议。请考虑到这些,因为它们与您当前设置的概念必要性有关。
从务实的角度来看,我还想补充几点(虽然我不认为这些会完全形成答案)。
首先,您的 BookAggregate
和 UserAggregate
包含相当多的状态。在执行 CQRS 时,我建议您的命令模型(阅读:聚合)只包含任务执行所需的状态。我的意思是,需要驻留在 BookAggregate
和 UserAggregate
中的唯一字段是您用来在 @CommandHandler
注释方法内部执行验证的字段。其余的都可以去,因为它没有被使用。此外,如果在稍后阶段需要它,您可以稍后简单地添加另一个事件源处理程序。这就是事件溯源的美妙之处;您可以在真正开始需要时从聚合事件流中已经存在的事件中添加状态。
其次,面向聚合间的通信。正如 Martin 所指出的,值得注意的是,聚合通常是命令模型。因此,它们仅处理命令和发布事件。 @EventSourcingHandler
注释方法可能表明您可以处理任何事件,但实际上,聚合只会处理它自己发布的事件。这适用于您需要重新创建聚合状态的那些事件。
现在,事件将允许您需要的聚合间通信。由于聚合仅使用它们自己的概念和状态,因此您必须引入另一个组件来对发布的事件做出反应以将命令分派到其他聚合。这个组件可以简单地是一个常规的事件处理组件(读作:一个带有 @EventHandler
注释函数的类)。如果您要响应的流程有时间概念,需要状态来执行其任务并与 N 个聚合进行通信?然后您可以使用 Saga 为例。
第三,也是最重要的,是决定你是否真的需要在你的系统中有两个不同的聚合。这就是我为@martingerber 他的回答鼓掌的原因。
尽管如此,这是我的两分钱。希望能帮到你。