原文地址:http://www.geekabyte.io/2015/10/exploring-cqrs-with-axon-framework_13.html
上一篇文章提供了实体,聚合,聚合根,存储库和域事件的基本解释,这些是域驱动设计中的一些构建块。 本文着眼于使用Axon Framework创建一些构建块。
在探索命令,命令总线和命令处理程序时,我们使用Axon查看了CQRS的命令处理方面,我们创建了命令,通过命令网关将其分派到命令总线,然后由各自的命令处理程序处理。
但在那篇文章中,命令处理程序所做的就是打印到控制台。 在现实生活中的应用程序中,您可能希望命令导致某些业务逻辑被执行,这可能导致应用程序中的状态更改。
实现这一点,就是我们在这篇文章中所看到的。
以下是示例应用程序
exploringCQRSwithAxon 是一个关于Github的项目,其中一个简单的应用程序旨在说明与Axon一起探索CQRS的系列文章中涉及的主题。
要关注这篇文章以及随附的项目,
首先克隆项目:
git clone git@github.com:dadepo/exploringCQRSwithAxon.git
然后检查相应的提交哈希,让项目处于说明本文所涉及主题的状态:
git checkout bccb3bbc796a7ec9c0b3747da3f2650ff03d09b4.
这篇文章的目的
这篇文章的目标是扩展示例应用程序,以便它的命令处理组件可以导致应用业务逻辑和状态更改。 这将看到我们添加一个存储库并构建实体。
基本上CQRS图的部分如下所示:
在这篇文章的最后,我们可以借记/贷记我们的虚拟账户(状态变化),我们将能够在浏览器中查看他们的余额。
在其他方面,为了实现我们的目标,对代码进行了一些更改。 我现在经历了主要的变化。
增加 Account 类.
@Entity
public class Account extends AbstractAggregateRoot {
@Id
private String id;
@Column
private Double balance;
public Account() {
}
public Account(String id) {
this.id = id;
this.balance = 0.0d;
}
/**
* Business Logic
* Cannot debit with a negative amount
* Cannot debit with more than a million amount (You laundering money?)
* @param debitAmount
*/
public void debit(Double debitAmount) {
if (Double.compare(debitAmount, 0.0d) > 0 &&
this.balance - debitAmount > -1) {
this.balance -= debitAmount;
} else {
throw new IllegalArgumentException("Cannot debit with the amount");
}
}
/**
* Business Logic
* Cannot credit with a negative amount
* Cannot credit with amount that leaves the account
* balance in a negative state
* @param creditAmount
*/
public void credit(Double creditAmount) {
if (Double.compare(creditAmount, 0.0d) > 0 &&
Double.compare(creditAmount, 1000000) < 0) {
this.balance += creditAmount;
} else {
throw new IllegalArgumentException("Cannot credit with the amount");
}
}
public Double getBalance() {
return balance;
}
public void setIdentifier(String id) {
this.id = id;
}
@Override
public Object getIdentifier() {
return id;
}
}
这是我们的实体。 而实体的状态我们将坚持下去。 我们还有信用卡和借记卡方法,它们都实现了信用卡和借记卡业务逻辑,作为实体的一部分。
你还会注意到两件事。 我们正在使用@Entity注释,我们的类正在扩展一个Axon类:AbstractAggregateRoot。
我们有@Entity注释,因为我们选择将我们的实体建模为JPA实体,这意味着持久层将是JPA。
我们有Account类扩展AbstractAggregateRoot,因为Account类已被指定为Aggregate Root。 正如前一篇文章中所定义的,聚合是对象及其聚合根的集合,聚合可以只是一个对象,在这种情况下,对象将是聚合根:帐户是聚合中唯一的对象。
那么为什么Account类会扩展AbstractAggregateRoot呢?
这是因为,即使帐户已被注释为JPA实体,我们也不会直接使用JPA的实体管理器与其进行交互。 我们的交互将使用Axon的存储库基础结构完成,而Axon的存储库基础结构要求聚合根类型为AggregateRoot,这正是我们通过扩展AbstractAggregateRoot实现的,因为它是AggregateRoot的实现。
值得注意的是,Axon提供了几个AggregateRoot的实现。 AbstractAggregateRoot只是其中之一,它适用于JPA支持的实体。
接下来我们需要做的是查看允许我们检索和持久化Account实体的存储库。
添加 repository
通常,如果我们不使用Axon Framework,我们可以直接使用EntityManager构建我们的存储库(或使用类似Spring JPA Data的东西)。 但由于我们使用的是Axon提供的基础架构,因此我们使用了Axon实现的JPA支持的存储库:GenericJpaRepository。
GenericJpaRepository需要使用EntityManager进行配置。 这是通过SimpleEntityManagerProvider完成的。 配置的添加显示了我们如何连接GenericJpaRepository并使用EntityManager配置它。
/**
* Configures a GenericJpaRepository as a Spring Bean. The Repository would
* be used to access
* the Account entity.
*
*/
@Bean
public GenericJpaRepository genericJpaRepository() {
SimpleEntityManagerProvider entityManagerProvider =
new SimpleEntityManagerProvider(entityManager);
return new GenericJpaRepository(entityManagerProvider, Account.class);
}
Note: We also updated our initialization code to add two Accounts entity for us on startup.
@Component
public class Db {
@Autowired
@Qualifier("transactionManager")
protected PlatformTransactionManager txManager;
@Autowired
private Repository repository;
@PostConstruct
private void init(){
TransactionTemplate transactionTmp = new TransactionTemplate(txManager);
transactionTmp.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
UnitOfWork uow = DefaultUnitOfWork.startAndGet();
repository.add(new Account("acc-one"));
repository.add(new Account("acc-two"));
uow.commit();
}
});
}
}
我们还将使用内存数据库,因此我们将内存数据库中的h2添加到依赖项中:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Spring Boot application.properties文件也会更新为包含所需的设置:
# Datasource configuration
spring.datasource.url=jdbc:h2:mem:exploredb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.validation-query=SELECT 1;
spring.datasource.initial-size=2
spring.datasource.sql-script-encoding=UTF-8
spring.jpa.database=h2
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
spring.freemarker.cache=false
通过所有这些设置,我们准备将我们的存储库引入我们的命令处理程序,而不是让命令处理程序将东西打印到控制台中,就像他们在使用Axon Framework探索CQRS时所做的那样:命令,命令总线和命令处理组件,我们 现在可以让他们做一些有用的事情,比如从存储库中检索帐户,更改状态并将其存储回来。
例如,我们的CreditAccountHandler现在看起来像这样:
@Component
public class CreditAccountHandler {
@Autowired
private Repository repository;
@CommandHandler
public void handle(CreditAccountCommand creditAccountCommandCommand){
Account accountToCredit = (Account) repository
.load(creditAccountCommandCommand.getAccount());
accountToCredit.credit(creditAccountCommandCommand.getAmount());
}
}
和DebitAccountHolder看起来像这样:
@Component
public class DebitAccountHandler {
@Autowired
private Repository repository;
@CommandHandler
public void handle(DebitAccountCommand debitAccountCommandCommand){
Account accountToDebit = (Account) repository
.load(debitAccountCommandCommand.getAccount());
accountToDebit.debit(debitAccountCommandCommand.getAmount());
}
}
我们注入了Repository(GenericJpaRepository),因为这是我们在上下文中提供的唯一实现。
然后,我们使用此存储库加载帐户实体,调用debit()或credit()方法,该方法应用更改状态的业务逻辑。
请注意,一旦我们从存储库中检索了帐户,我们就不必在其他方面显式调用save或persist方法来保持我们所做的更改。 命令处理基础结构与Axon存储库一起使用,以确保在Command Handling方法返回后自动完成实体的持久性。
如果你检查处理程序方法中的逻辑,你会意识到它正在做的只是:
从存储库中检索聚合根
在保存的对象上调用所需的方法。
这两个步骤中最重要的操作是在Aggregate Root上实际执行所需的方法,因此,Axon Framework支持一种机制,您无需执行检索Aggregate Root即可调用 所需的方法。 这是通过将@CommandHandler放在Aggregate Root本身的方法上来实现的。 在这种情况下,Axon将负责从存储库中检索对象并调用所需的方法。
在这种情况下,您不需要将命令处理程序作为明显的单独组件,这就是为什么在此解释中不使用该方法的原因。 但在实际应用中,直接在Aggregate Root上使用@CommandHandler应该是推荐的方法。
剩下部分的代码的更新是对视图层进行的。
我们添加了另一个控制器:ViewController,这次,它直接使用EntityManager来检索用于显示的帐户的状态,这是正常的,因为我们将它用于视图:
@Controller
public class ViewController {
@Autowired
private EntityManager entityManager;
@RequestMapping(value = "/view",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public List<Map<String, Double>>
getAccounts(@RequestBody List<String> accountsNumber) {
List<Map<String, Double>> accounts = new ArrayList<>();
accountsNumber.stream().forEach(no -> {
Account account = entityManager.find(Account.class, no);
Map<String, Double> accountMap = new HashMap<>();
accountMap.put((String) account.getIdentifier(),
account.getBalance());
accounts.add(accountMap);
});
return accounts;
}
}
然后我们更新了客户端的JavaScript以轮询 /view url以获取两个帐户的状态:
$.ajax({
url: "/view",
method: "POST",
contentType: "application/json",
data: JSON.stringify(["acc-one", "acc-two"]),
success: function(accounts) {
var html = "";
accounts.forEach(function(account){
for (var key in account) {
html += "<tr><td>" + key + "</td><td>"
+ account[key] + "</td></tr>"
}
});
$("table#balance tbody").html(html);
},
error: function(a) {
console.log(a);
}
});
}, 2000);
如果您启动应用程序,您将获得类似于以下屏幕截图的内容:
现在,当您借记或贷记任何帐户时,您会看到这几乎立即反映在浏览器中。
Overview of the Axon Building Blocks
AggregateRoot
这是Axon实现中的接口聚合。 实现此接口使Axon的存储库可以执行检索,存储和发布域事件等任务。
AbstractAggregateRoot
大多数(如果不是全部)Axon的核心接口作为其构建块,其实现应该满足一般用例,并且应该让您快速入门。 AbstractAggregateRoot就是这样一种实现。 它是AggregateRoot接口的实现。 AbstractAnnotatedAggregateRoot和AbstractEventSourcedAggregateRoot是Axon提供的AggregateRoot的其他实现。
GenericJpaRepository
一种使用JPA的Axon存储库,因为它提供了持久化技术。 它必须使用JPA EntityManager进行配置。
EntityManagerProvider and SimpleEntityManagerProvider
EntityManagerProvider是一个接口组件,提供对EntityManager的访问。 SimpleEnttityManagerProvider是Axon Framework提供的实现。
Summary
我们完成了什么? 让我们再说一遍。
我们现在有一个设置,允许我们发出命令,让命令处理程序响应命令。 我们的命令处理程序现在使用Axon存储库基础结构来检索实体,更改其状态并将其保留。
需要注意的一件重要事情是:这还不是CQRS。
我们还没有达到CQRS的核心,即write/command 基础设施与query/read基础设施的分离,因为我们仍在访问同一个物理存储,我们仍然使用相同的模型(帐户实体) 我们用于命令的视图。
下一篇文章将是使用Axon Framework探索CQRS:介绍域事件和事件处理。 在其中,我们通过引入域事件和事件处理来研究如何使我们的设置更加符合CQRS。