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 necessaryprivate String id;// fields containing state...@CommandHandlerpublic MyAggregate(CreateMyAggregateCommand command) {// ... update stateapply(new MyAggregateCreatedEvent(...));}// constructor needed by JPAprotected MyAggregate() {}
}
聚合中的实体可以通过定义@eventhandler注释方法来侦听聚合发布的事件。当事件消息发布(在发布任何外部处理程序之前)将调用这些方法。

3. Event sourced aggregates(事件源聚合)

除了存储聚合的当前状态之外,还可以根据它在过去发布的事件来重建聚合状态。为此,所有状态的更改必须由一个事件表示。
对于主要部分,事件源聚合类似于“常规”聚合:它们必须声明一个标识符,并可以使用apply方法来发布事件。然而,事件源聚合(即任何字段值的任何更改)的状态更改必须在@eventsourcingHandler注释的方法中进行。这包括设置聚合的标识符。
注意,聚合标识符必须设置在由聚合所发布的第一个事件的@eventsourcinghandler中。这通常是创建事件。
事件源聚合的聚合根也必须包含无参数构造函数。Axon框架使用此构造函数在使用过去的事件初始化聚合之前创建一个空聚合实例。如果未能提供此构造函数,则在加载聚合时将导致异常。
public class MyAggregateRoot {@AggregateIdentifierprivate String aggregateIdentifier;// fields containing state...@CommandHandlerpublic MyAggregateRoot(CreateMyAggregate cmd) {apply(new MyAggregateCreatedEvent(cmd.getId()));}// constructor needed for reconstructionprotected MyAggregateRoot() {}@EventSourcingHandlerprivate void handleMyAggregateCreatedEvent(MyAggregateCreatedEvent event) {// make sure identifier is always initialized properlythis.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 {@AggregateIdentifierprivate String id;@CommandHandlerpublic MyAggregate(CreateMyAggregateCommand command) {apply(new MyAggregateCreatedEvent(IdentifierFactory.getInstance().generateIdentifier()));}// no-arg constructor for AxonMyAggregate() {}@CommandHandlerpublic void doSomething(DoSomethingCommand command) {// do something...}// code omitted for brevity. The event handler for MyAggregateCreatedEvent must set the id field
}public class DoSomethingCommand {@TargetAggregateIdentifierprivate 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 {@AggregateIdentifierprivate String id;@AggregateMemberprivate MyEntity entity;@CommandHandlerpublic MyAggregate(CreateMyAggregateCommand command) {apply(new MyAggregateCreatedEvent(...);}// no-arg constructor for AxonMyAggregate() {}@CommandHandlerpublic 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 {@CommandHandlerpublic 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 {@CommandHandlerpublic 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());

Axon Framework官方文档(五)相关推荐

  1. StackExchange.Redis 官方文档(五) Keys, Values and Channels

    StackExchange.Redis 官方文档(五) Keys, Values and Channels 原文:StackExchange.Redis 官方文档(五) Keys, Values an ...

  2. Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(二)

    接前一篇 Spring Framework 官方文档学习(四)之Validation.Data Binding.Type Conversion(一) 本篇主要内容:Spring Type Conver ...

  3. Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion

    本篇太乱,请移步: Spring Framework 官方文档学习(四)之Validation.Data Binding.Type Conversion(一) 写了删删了写,反复几次,对自己的描述很不 ...

  4. Spring指南之使用Spring缓存数据(Spring Framework官方文档之缓存抽象详解)

    1.请参见官方文档Spring指南之使用 Spring 缓存数据 2.请参见Spring官方文档之缓存抽象 3.参见github代码 文章目录 一.简介 二.你将创造什么(What You Will ...

  5. Spring Framework 官方文档学习(三)之Resource

    起因 标准JDK中使用 java.net.URL 来处理资源,但有很多不足,例如不能限定classpath,不能限定 ServletContext 路径. 所以,Spring提供了 Resource ...

  6. ABP官方文档(五)【多租户】

    1.5 ABP总体介绍 - 多租户 1.5.1 什么是多租户 维基百科:"软件多租户是指一个软件架构的实例软件运行在一个服务器上,但存在多个租户.租户是一组共享一个公共的用户访问特定权限的软 ...

  7. Sass快速入门笔记(将主要知识点截取出来,参考官方文档和一些网络教学视频)

    文章目录 一.安装Sass(全部参考Sass官方文档) 1.windows需先安装`RuBy`,`Mac`系统自带`RuBy`无需安装 2.Sass安装 二.编译css文件 1.单个文件编译(命令行) ...

  8. Flask官方文档学习--从零开始解读(一)

    介绍 Flask是一个基于Python语言的轻量级Web框架,与之经常对比的框架还有Django.Tornado等框架,当然学习这些肯定首先要有一点Python基础,当然由于框架带来的优越性,通常在实 ...

  9. ExoPlayer详解——高级主题(官方文档)

    ExoPlayer详解系列文章 ExoPlayer详解--入门(官方文档) ExoPlayer详解--媒体类型(官方文档) ExoPlayer详解--高级主题(官方文档) 一.数字版权管理 ExoPl ...

最新文章

  1. 在网易有道做语音算法工程师是一种怎样的体验?
  2. python应用:最长无重复字串提取
  3. 一文读懂Java 11的ZGC为何如此高效
  4. Py之cairocffi:cairocffi的简介、安装、使用方法之详细攻略
  5. mybatis 一对一与一对多collection和association的使用
  6. 如何使 FlashGet 正常合法 下载 Session 中的自定义文件链接呢? JSP/Servlet 实现!
  7. 80-10-015-原理-Java NIO-ByteBuffer
  8. win10 全局快捷键设置启动程序
  9. php 月份英文,所有月份的英语单词
  10. 乘2取整法_十进制小数转换成二进制小数,可以采用“乘2取整”法
  11. strcmp()函数详解
  12. c语言字符怎么运算,c语言运算符号(c语言如何输入运算符号)
  13. Centos8修改mysql密码
  14. 南开大学计算机提前批,提前批几家985/211高校爆出冷门,考生:后悔没报最后一所...
  15. PTA 1032 挖掘机技术哪家强 (c语言)
  16. 2021年G2电站锅炉司炉考试试卷及G2电站锅炉司炉证考试
  17. php用css改变字体,css怎么设置字体立体
  18. 区块链,开启数字时代的金钥匙
  19. 连接IBM MQ原因码报2537的错误解决记录
  20. ARP9-Licensee

热门文章

  1. 新C++(5):异常
  2. 多个日期时间段进行合并计算时长,剔除重叠时间段
  3. java内存空间详解
  4. 三菱FX2NPLC CMP比较指令
  5. 制作各种docker镜像
  6. 李艾30场直播数据全解析,挖掘直播高转化技巧
  7. 你们怎么都有自己的聊天机器人?给我也来一个!
  8. vue-seamless-scroll遇到一些问题
  9. gedit 显示行号
  10. 3级流水线11位-4位CRC循环冗余校验码生成器Verilog