使用Axon Framework探索CQRS架构系列(三):构建实体、聚合、聚合根、存储库组件

原文地址: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。

猜你喜欢

转载自blog.csdn.net/quguang65265/article/details/81368321