5.Command Model
在基于CQRS的应用程序中,一个领域模型(由Eric Evans和Martin Fowler提出的概念)可以是一种非常强大的机制,它可以利用状态更改的验证和执行所涉及的复杂性。虽然典型的领域模型有大量的构建块,但其中一个在CQRS中应用于命令处理时起着主导作用:聚合。
应用程序中的状态更改以命令开始。命令是表达意图(描述您想要完成的),以及基于该意图进行操作所需的信息的组合。命令模型用于处理传入命令,以验证它并定义结果。在这个模型中,一个命令处理程序负责处理特定类型的命令,并根据包含的信息采取行动。
5.1 Aggregate(聚合)
集合是一个实体或一组实体,它们总是保持一致的状态。聚合根是在聚合树之上的对象,它负责维护这个一致的状态。这使得在任何基于CQRS的应用程序中实现命令模型的主要构建块都是这样的。
Note:
“Aggregate”一词指的是由Evans在领域驱动设计中定义的聚合:
作为数据更改目的而被当作一个单元的一组关联对象。外部引用仅限于对聚合根的引用。在聚合的衔接上下文之内应用一组一致性规则。
例如,“Contact”聚合体可以包含两个实体:Contact和Address。为了使整个聚合保持一致状态,在contact(联系人)中添加address(地址)应该通过contact(联系人)实体来完成。在这种情况下,contact(联系人)实体是指定的聚合根。
在Axon中,聚合由聚合标识符标识。它可能是任何对象,但也有一些标识符良好实现的指导原则。标识符必须:
>实现equals和hashCode,以确保与其他实例进行良好的等式比较,
>实现toString(),提供一致的结果(相等的标识符所提供的toString()结果).
>最好是实现Serializable
测试装置(参见官方文档八)将验证这些条件,并在聚合使用不兼容的标识符时失败。字符串、UUID和数字类型的标识符都是合适的。不要使用原始类型作为标识符,因为它们不允许懒加载。在某些情况下,Axon可能会错误地假设原始类型的默认值是标识符的值。
Note:
使用随机生成的标识符是一种很好的做法,而不是用顺序生成的标识符。使用一个序列极大地降低了应用程序的可伸缩性,因为机器需要保持最后一个使用序列号的更新。与一个UUID碰撞的机会很小(如果你产生8.2×10^11个UUID,只有10^−15的机会产生冲突)。
此外,在使用聚合的函数标识符时要小心。他们有改变的倾向,因此很难调整你的应用程序。
5.2 Aggregate implementations(聚合实现)
聚合总是通过一个实体访问,称为聚合根。通常情况下,这个实体的名称和整个聚合的名称相同。例如,Order聚合可能包含一个Order实体,而Order引用了几个OrderLine实体。Order和OrderLine一起,组成了聚合。
聚合是一个常规对象,它包含状态以及可以改变状态的方法。虽然按照CQRS原则来说这是不完全正确的,但也可以通过访问器方法暴露出聚合的状态。
聚合根必须声明包含聚合标识符的字段。这个标识符必须在第一个事件发布时被初始化。这个标识符字段必须由@ aggregateidentifier注释注释。如果您使用JPA并在聚合上使用JPA注释,那么Axon还可以使用JPA提供的@id注释。
@Entity // Mark this aggregate as a JPA Entity
public class MyAggregate {
@Id // When annotating with JPA @Id, the @AggregateIdentifier annotation is not necessary
private String id;
// fields containing state...
@CommandHandler
public MyAggregate(CreateMyAggregateCommand command) {
// ... update state
apply(new MyAggregateCreatedEvent(...));
}
// constructor needed by JPA
protected MyAggregate() {
}
}
聚合中的实体可以通过定义@eventhandler注释方法来侦听聚合发布的事件。当事件消息发布(在发布任何外部处理程序之前)将调用这些方法。
3. Event sourced aggregates(事件源聚合)
除了存储聚合的当前状态之外,还可以根据它在过去发布的事件来重建聚合状态。为此,所有状态的更改必须由一个事件表示。
对于主要部分,事件源聚合类似于“常规”聚合:它们必须声明一个标识符,并可以使用apply方法来发布事件。然而,事件源聚合(即任何字段值的任何更改)的状态更改必须在@eventsourcingHandler注释的方法中进行。这包括设置聚合的标识符。
注意,聚合标识符必须设置在由聚合所发布的第一个事件的@eventsourcinghandler中。这通常是创建事件。
事件源聚合的聚合根也必须包含无参数构造函数。Axon框架使用此构造函数在使用过去的事件初始化聚合之前创建一个空聚合实例。如果未能提供此构造函数,则在加载聚合时将导致异常。
public class MyAggregateRoot {
@AggregateIdentifier
private String aggregateIdentifier;
// fields containing state...
@CommandHandler
public MyAggregateRoot(CreateMyAggregate cmd) {
apply(new MyAggregateCreatedEvent(cmd.getId()));
}
// constructor needed for reconstruction
protected MyAggregateRoot() {
}
@EventSourcingHandler
private void handleMyAggregateCreatedEvent(MyAggregateCreatedEvent event) {
// make sure identifier is always initialized properly
this.aggregateIdentifier = event.getMyAggregateIdentifier();
// ... update state
}
}
@eventsourcingHandler注解方法使用特定的规则解析。这些规则对于@eventHandler注释的方法是一样的,并且在Annotated Event Handler中会详细的解释。
Note:
事件处理程序方法可能是私有的,只要JVM的安全设置允许Axon框架改变方法的可访问性。这允许您清楚地将聚合的公共API分离开来,这将从处理事件的内部逻辑中公开生成事件的方法。
大多数IDE都有一个选项,可以忽略“未使用的私有方法”警告,以使用特定的注释。或者,您可以向方法添加一个@SuppressWarnings("UnusedDeclaration")注解,以确保您不会不小心删除事件处理程序方法。
在某些情况下,特别是当聚合结构不仅仅是几个实体的时候,对在同一聚合体中的其他实体上发布的事件作出反应是比较干净的。然而,由于在重建聚合状态时也会调用事件处理程序方法,因此必须采取特别的预防措施。
可以在事件源处理器方法中apply()新事件。这使得实体B可以在对实体A的反应中应用事件。当重新播放历史事件时,Axon将忽略apply()调用。请注意,在这种情况下,内部apply()调用的事件只在所有实体收到第一个事件之后才被发布到实体。如果需要发布更多的事件,基于一个实体在应用内事件后的状态,使用apply(...).andThenApply(...)。
您还可以使用静态AggregateLifecycle.isLive()方法来检查聚合是否“live”。基本上,如果一个聚合完成了重新播放历史事件,它就被认为是活的。在重新播放这些事件时,isLive()将返回false。使用isLive()方法,您可以执行应该只在处理新生成的事件时完成的活动。
4. Complex Aggregate structures(复杂聚合结构)
复杂的业务逻辑通常需要的不仅仅是一个聚合根可以提供的聚合。在这种情况下,很重要的是,复杂性在聚合中分布在多个实体上。在使用事件源时,不仅聚合根需要使用事件来触发状态转换,而且聚合的每个实体也需要使用事件资源。
Note:
注意,聚合不应该公开状态的规则的一个常见错误解释是,所有实体都不应该包含任何属性访问器方法。事实并非如此。事实上,如果聚合体中的实体向同一集合中的其他实体公开状态,那么聚合体可能会获益良多。但是,建议不要在聚合之外公开状态。
Axon为复杂事件结构的事件源提供支持。实体就像聚合的根,简单的对象。声明子实体的字段必须用@aggregateMember注释。这个注释告诉Axon,带注释的字段包含一个应该检查命令和事件处理程序的类。
当一个实体(包括聚合根)apply一个事件时,它首先由聚合根来处理,然后穿过由@aggregateMember注解的属性到达它的子实体
包含子实体的字段必须用@aggregateMember注释。该注释可用于多个字段类型:
>实体类型,直接在字段中引用
>包含Iterable的字段(包括所有集合,如Set、List等);
>字段的值包含java.util.Map的
5.5. Handling commands in an Aggregate
建议在包含处理状态命令的聚合中直接定义命令处理器,因为命令处理器有可能需要该聚合的状态来执行其任务。
要在聚合中定义一个命令处理程序,只需用@commandhandler注释命令处理方法。@commandHandler注释方法的规则与任何处理程序方法相同。然而,命令不仅是由它们的 payload(负载)路由的。CommandMessage(命令消息)携带一个name(名称),该名称默认为命令对象的完全限定类名。
默认情况下,@CommandHandler 注释方法允许下列参数类型:
>第一个参数是命令消息的payload(有效负载)。如果@commandHandler注释显式定义了处理程序可以处理的命令的名称,那么它也可能是类型Message或CommandMessage。默认情况下,命令名是命令的有效负载的完全限定类名。
>使用@metadataValue注解的参数将以注解上显示的键来解析Meta-Data值。如果required是false(默认值),则在不存在元数据值时传递null。如果requiredshi true的话,解析器将无法匹配并防止在不存在元数据值时调用该方法。
>MetaData类型的参数获得由CommandMessage 注入的整个元数据
>UnitOfWork 类型的参数得到注入当前的工作单位。这允许命令处理程序在工作单元的特定阶段注册要执行的操作,或者访问已注册的资源。
>Message或者CommandMessage类型的参数,将会得到完整的消息,包括payload和meta-data。如果方法需要多个元数据字段或包装消息的其他属性,那么这是有用的。
为了使Axon知道哪一个聚合类型的实例应该处理命令消息,命令对象中的携带标识符的属性必须标注@TargetAggregateIdentifier注解。注解可以放在字段或访问器方法(例如getter)上。
创建聚合实例的命令不需要标识目标聚合标识符,尽管建议将其添加到它们的聚合标识符上。
如果您愿意使用另一种机制来执行路由命令,则可以通过提供定制的CommandTargetResolver来覆盖该行为。这个类应该根据给定的命令返回聚合标识符和期望的版本(如果有的话)。
PS:
当@CommandHandler注解放在聚合的构造函数上时,相应的命令将创建一个新的聚合实例,并将其添加到存储库中。这些命令不需要针对特定的聚合实例。因此,这些命令不需要任何@TargetAggregateIdentifier或@TargetAggregateVersion注解,也不会自定义CommandTargetResolver调用这些命令。
当一个命令创建一个聚合实例时,当命令成功执行时,该命令的回调将接收聚合标识符。
public class MyAggregate {
@AggregateIdentifier
private String id;
@CommandHandler
public MyAggregate(CreateMyAggregateCommand command) {
apply(new MyAggregateCreatedEvent(IdentifierFactory.getInstance().generateIdentifier()));
}
// no-arg constructor for Axon
MyAggregate() {
}
@CommandHandler
public void doSomething(DoSomethingCommand command) {
// do something...
}
// code omitted for brevity. The event handler for MyAggregateCreatedEvent must set the id field
}
public class DoSomethingCommand {
@TargetAggregateIdentifier
private String aggregateId;
// code omitted for brevity
}
可以使用Axon的配置API来配置聚合。例如
Configurer configurer = ...
// to use defaults:
configurer.configureAggreate(MyAggregate.class);
// allowing customizations:
configurer.configureAggregate(
AggregateConfigurer.defaultConfiguration(MyAggregate.class)
.configureCommandTargetResolver(c -> new CustomCommandTargetResolver())
);
@commandhandler注解并不局限于聚合根。在根目录中放置所有的命令处理程序有时会导致聚合根上的大量方法,而许多方法只是将调用转发给一个底层实体。如果是这样,那么您可以将@ commandhandler注释放在一个底层实体的方法上。对于Axon想要找到这些带注释的方法,那么你需要在聚合根中声明实体的字段上必须标识为@aggregatemember注解。请注意,只有被声明的带注释的字段才被用于命令处理程序。如果在传入的命令到达该实体时字段值为空,则抛出异常。
public class MyAggregate {
@AggregateIdentifier
private String id;
@AggregateMember
private MyEntity entity;
@CommandHandler
public MyAggregate(CreateMyAggregateCommand command) {
apply(new MyAggregateCreatedEvent(...);
}
// no-arg constructor for Axon
MyAggregate() {
}
@CommandHandler
public void doSomething(DoSomethingCommand command) {
// do something...
}
// code omitted for brevity. The event handler for MyAggregateCreatedEvent must set the id field
// and somewhere in the lifecycle, a value for "entity" must be assigned to be able to accept
// DoSomethingInEntityCommand commands.
}
public class MyEntity {
@CommandHandler
public void handleSomeCommand(DoSomethingInEntityCommand command) {
// do something
}
}
Note:
注意,每个命令必须在聚合中有一个处理器。这意味着您不能为同一个命令注解多个命令处理器(不管是在聚合根下还是子类中)。如果您需要有条件地将一个命令路由到一个实体,这些实体的父节点应该处理该命令,并根据应用的条件转发它。
字段的运行时类型不必完全是声明类型。然而,只有@aggregatemember注释的属性的类型才会被检查,以确定是否有@CommandHandler注解的方法
还可以用@AggregateMember注解包含实体的集合和映射。在后一种情况下,映射的值被期望包含实体,而键包含一个值,作为它们的引用。
当需要将命令路由到正确的实例时,必须正确地识别这些实例。他们的“id”字段必须用@entityid来标注。命令中的属性将用于查找消息应该被路由到的实体,默认为注解的字段的名称。例如,在注解字段“myEntityId”时,命令对象必须使用相同的名称定义一个属性。这意味着必须存在getMyEntityId或myEntityId()方法。如果字段的名称和路由属性不同,则可以使用@EntityId(routingKey = "customRoutingProperty").显式地提供值。
如果在带注解的集合或map中没有发现任何实体,那么Axon就会抛出一个IllegalStateException;显然,在那个时候,聚合无法处理该命令。
Note:
集合或map的字段声明应该包含适当的泛型,以允许Axon识别包含在集合或映射中的实体类型。如果不可能在声明中添加泛型(例如,因为您使用的是已经定义泛型类型的自定义实现),您必须指定在@aggregatemember注释中使用的entityType属性中使用的实体类型。
5.6 External Command Handlers(外部命令处理器)
在某些情况下,不可能将命令直接路由到聚合实例。在这种情况下,可以注册一个命令处理程序对象。
命令处理程序对象是一个简单的(普通的)对象,它有@ commandhandler注释的方法。与聚合的情况不同的是,只有一个命令处理程序对象的实例,它处理在它方法中声明的所有类型的命令。
public class MyAnnotatedHandler {
@CommandHandler
public void handleSomeCommand(SomeCommand command, @MetaDataValue("userId") String userId) {
// whatever logic here
}
@CommandHandler(commandName = "myCustomCommand")
public void handleCustomCommand(SomeCommand command) {
// handling logic here
}
}
// To register the annotated handlers to the command bus:
Configurer configurer = ...
configurer.registerCommandHandler(c -> new MyAnnotatedHandler());