使用Axon Framework探索CQRS架构系列(五): 事件溯源 Event Sourcing的应用

原文地址:http://geekabyte.blogspot.nl/2015/10/exploring-cqrs-with-axon-framework_29.html

这篇文章是使用Axon Framework探索CQRS架构的系列文章的一部分。 它考虑使用Axon Framework将CQRS中的事件溯源结合起来。

本系列的前几篇文章探讨了CQRS应用程序的各个组成部分,在介绍域事件和事件处理方面,我们能够将所有内容放在一起。 我们在没有引入事件溯源的情况下完成了所有这些工作,表明可以轻松拥有CQRS架构而无需引入事件溯源。 但在这篇文章中,我们这样做:介绍事件溯源。

在 Github上项目地址 (exploringCQRSwithAxon) ,伴随着这些系列的文章。 这是一个简单的应用程序,我们可以在两个虚拟账户之间进行借记,贷记和转账。 该项目中的代码旨在帮助说明Axon框架在构建CQRS应用程序中的应用。

Following with the sample application

要使示例应用程序处于此帖子内容适用的状态:
首先克隆项目:

git clone git@github.com:dadepo/exploringCQRSwithAxon.git

然后check out到需要的 提交hash

git checkout fef500cab7b6129d2e12c14b07d28265aa99f219

该应用程序是使用带有Spring Boot的Axon Framework构建的。 在其他运行它,只需更改到根目录并执行mvn spring boot:run

目标

我们在这篇文章中的目标是修改exploreCQRSwithAxon应用程序并引入事件溯源。

But what is Event Sourcing?

那么我必须承认为什么使用这个项目作为示例项目,伴随这些系列文章,使用银行账户比喻的原因是因为银行账户交易是我见过的最有效的例子之一,可以很容易地用来解释 事件溯源…..

因此,要解释事件溯源,想象一下银行帐户。 银行账户的当前状态很可能不会是保存在数据库中某个地方的数值,相反,它很可能是通过计算所有发生的变化而得到的值:贷记和借记 发生在帐户上。

这就是Event Sourcing。 这是保存更改域模型状态的所有操作的方法,并使用这些保存的更改在需要时重建或到达模型的当前状态。

因此,使用Event Sourcing,我们不会“存储”系统的当前状态,而是存储更改系统的所有事件。 当我们需要获取当前状态时,我们检索系统的初始状态并应用对系统有任何影响的所有已保存事件。

Axon框架带有必要的基础架构组件,可以轻松地将事件溯源引入应用程序。

我们该怎么做呢?

总之,我们引入事件源的方式,涉及使用不存储域对象的当前状态,但存储从它们发出的域事件的存储库。 这些类型的存储库知道如何从存储的事件构造聚合的当前状态,因此每当我们想要检索聚合以供使用时,我们不需要手动应用这些事件。

Axon Framework提供了这样的存储库实现。

这是一般要点,现在让我们来看看将事件溯源引入我们的应用程序的代码所要做的更改:

Configuring the Event Store

由于我们的存储库将存储域事件而不是域对象的当前状态,因此我们需要为其提供事件存储。 EventStore接口定义事件存储机制。 Axon Framework提供了几个EventStore接口的实现,具体取决于事件的存储方式:从允许在文件系统,MongoDB,Cassandra中存储事件的实现,或者允许使用JPA或JDBC等的关系数据库在事件中存储事件的实现。

在这篇文章中,我们将使用FileSystemEventStore,我们将其配置为Spring bean:

/**
* An event sourcing implementation needs a place to store events. i.e. 
* The event Store.
* In our use case we will be storing our events in a file system, so we 
* configure
* the FileSystemEventStore as our EventStore implementation
*
* It should be noted that Axon allows storing the events
* in other persistent mechanism...jdbc, jpa etc
*
* @return the {@link EventStore}
*/
@Bean
public EventStore eventStore() {
 EventStore eventStore = 
   new FileSystemEventStore(new SimpleEventFileResolver(new File("./events")));
  return eventStore;
}

从上面可以看出,我们使用SimpleEventFileResolver来配置存储事件流的位置。

Configure the EventSourcingRepository

由于我们不再存储当前状态,因此我们删除了GenericJpaRepository并且包含了EntityManager的自动装配。

您还会注意到spring-boot-starter-data-jpa依赖项已被spring-boot-starter-jdbc替换。

然后,我们配置了一个EventSourcingRepository,它可以从保存在事件源中的事件流中检索和保存实体。 配置如下:

/**
* Our aggregate root is now created from stream of events and not 
* from a representation in a persistent mechanism,
* thus we need a repository that can handle the retrieving of our 
* aggregate root from the stream of events.
*
* We configure the EventSourcingRepository which does exactly this. 
* We supply it with the event store

* @return {@link EventSourcingRepository}
*/
@Bean
public EventSourcingRepository eventSourcingRepository() {
  EventSourcingRepository eventSourcingRepository = 
        new EventSourcingRepository(Account.class, eventStore());
  eventSourcingRepository.setEventBus(eventBus());
  return eventSourcingRepository;
}

请注意,我们提供了之前配置的EventStore。 此外,我们仍然使用eventbus配置存储库,因为它仍将发布域事件。

Db#init()方法也更新为始终在应用程序启动时删除事件存储中生成的事件。 这是为了确保我们在启动应用程序时始终保持清晰。

Changes to Account

然后我们更新我们的聚合根。 我们删除了所有与JPA相关的注释(@ Entity,@ Id,@ Column),然后我们引入了@AggregateIdentifier。

@AggregateIdentifier扮演类似于JPA的@Id的角色,它用于标记用于维护实体身份的字段。 接下来我们使用Account扩展AbstractAnnotatedAggregateRoot而不是AbstractAggregateRoot

我们必须使用AbstractAnnotatedAggregateRoot替换聚合根扩展的类,因为AbstractAggregateRoot不支持事件溯源。

然后我们介绍apply()方法。 apply方法在功能上与registerEvent()方法类似,但在事件源的用例中,apply()方法不仅生成域事件,还确保它存储在后台事件存储中。

借记debit方法现在看起来如此:

public void debit(Double debitAmount) {

 if (Double.compare(debitAmount, 0.0d) > 0 &&
       this.balance - debitAmount > -1) {
   /**
    * Instead of changing the state directly we apply an event
    * that conveys what happened.
    *
    * The event thus applied is stored.
    */
   apply(new AccountDebitedEvent(this.accountNo, debitAmount, this.balance));
  } else {
   throw new IllegalArgumentException("Cannot debit with the amount");
  }
}

然后 credit 方法看起来如此:

public void credit(Double creditAmount) {

 if (Double.compare(creditAmount, 0.0d) > 0 &&
       Double.compare(creditAmount, 1000000) < 0) {
  /**
   * Instead of changing the state directly we apply an event
   * that conveys what happened.
   *
   * The event thus applied is stored.
   */
   apply(new AccountCreditedEvent(this.accountNo, creditAmount, this.balance));
 } else {
   throw new IllegalArgumentException("Cannot credit with the amount");
 }
}

我们还介绍了两个新事件:AccountCreditedEvent和AccountDebitedEvent。

可以看出,我们“应用事件”而不是改变状态。 这些事件将是事件源。

然后我们介绍了两种新方法,它们是我们引入的新事件的处理程序:

@EventSourcingHandler
private void applyDebit(AccountDebitedEvent event) {
 /**
  * This method is called as a reflection of applying events stored in 
  * the event store.
  * Consequent application of all the events in the event store will 
  * bring the Account
  * to the most recent state.
  */

  this.balance -= event.getAmountDebited();
}

and

@EventSourcingHandler
private void applyCredit(AccountCreditedEvent event) {
 /**
  * This method is called as a reflection of applying events stored 
  * in the event store.
  * Consequent application of all the events in the event store will 
  * bring the Account
  * to the most recent state.
  */
 this.balance += event.getAmountCredited();
}

and it is in these methods that the actual state changes is done. Note that these methods are annotated with @EventSourcingHandler. Which is slightly different from @EventSourcing we have been introduced to in previous posts in the sense that the @EventSourcingHandler is meant to mark methods that react to changes within an aggregate.

Before we go forward let us take another look again at the changes thus far and perhaps explain all the moving parts we just introduced?

So we started by removing the business logic that actually changes state from the debit() and credit() methods. Instead we introduce the usage of apply() method which we used to apply the AccountCreditedEvent and AccountDebitedEvent, technically publishing these events as domain events and storing them as part of event sourcing.

Although we are no longer changing states in the debit/credit method, we still need to apply the state changing logic. To do this, we added two new methods which we annotated with @EventSourcingHandler. We moved the state changing logic there instead.

The @EventSourcingHandler marks a method as one that contains logic to be applied to an aggregate based on domain events generated by that aggregate.

When an event sourced aggregate is being built to it’s current states, all the events stored by using the apply() method is replayed and the methods annotated by @EventSourcingHandler are executed. This is how the current state is achieved.

在这些方法中,实际的状态变化已经完成。 请注意,这些方法使用@EventSourcingHandler进行注释。 这与@EventSourcing略有不同,因为@EventSourcingHandler用于标记对聚合中的更改做出反应的方法,因此我们在之前的帖子中已经介绍过。

在我们前进之前,让我们再看看迄今为止的变化,或许可以解释我们刚才介绍的所有改动的组件

因此,我们首先从debit()credit()方法中删除实际更改状态的业务逻辑。 相反,我们介绍了apply()方法的用法,我们用它来应用AccountCreditedEvent和AccountDebitedEvent,技术上将这些事件作为域事件发布并将它们存储为事件源的一部分。

虽然我们不再在借记/贷记方法中改变状态,但我们仍然需要应用状态改变逻辑。 为此,我们添加了两个使用@EventSourcingHandler注释的新方法。 我们改变了状态改变逻辑。

@EventSourcingHandler将方法标记为,包含基于该聚合生成的域事件应用于聚合的逻辑的方法。

当为其当前状态构建事件源聚合时,将重放使用apply()方法存储的所有事件,并执行@EventSourcingHandler注释的方法。 这就是当前状态的实现方式。

然后我们修改非默认构造函数。 我们应用新事件AccountCreditedEvent。

public Account(String accountNo) {
  apply(new AccountCreatedEvent(accountNo));
}

with it’s accompanying @EventSourcingHandler method

@EventSourcingHandler
public void applyAccountCreation(AccountCreatedEvent event) {
  this.accountNo = event.getAccountNo();
  this.balance = 0.0d;
}

Updates to the Event Handling Logic

通过上述修改,我们不再存储当前状态,我们存储事件。 因此,我们还需要更新事件处理逻辑,以便能够利用在其他帐户被借记/贷记时发布的域事件,以便能够准确地反映视图层将查询的系统的当前状态。

AccountCreditedEventHandler现在看起来如此:

@Component
public class AccountCreditedEventHandler {

 @Autowired
 DataSource dataSource;

 @EventHandler
 public void handleAccountCreditedEvent(AccountCreditedEvent event, 
                   Message eventMessage, @Timestamp DateTime moment) {
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

    // Get the current states as reflected in the event
    String accountNo = event.getAccountNo();
    Double balance = event.getBalance();
    Double amountCredited = event.getAmountCredited();
    Double newBalance = balance + amountCredited;

    // Update the view
    String updateQuery = "UPDATE account_view SET balance = ? 
                                                  WHERE account_no = ?";
    jdbcTemplate.update(updateQuery, new Object[]{newBalance, accountNo});

    System.out.println("Events Handled With EventMessage " + 
                      eventMessage.toString() + " at " + moment.toString());
  }

}

the AccountDebitedEventHandler now look thus:

@Component
public class AccountDebitedEventHandler {

  @Autowired
  DataSource dataSource;

  @EventHandler
  public void handleAccountDebitedEvent(AccountDebitedEvent event) {
      JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

      // Get the current states as reflected in the event
      String accountNo = event.getAccountNo();
      Double balance = event.getBalance();
      Double amountDebited = event.getAmountDebited();
      Double newBalance = balance - amountDebited;

      // Update the view
      String updateQuery = "UPDATE account_view SET balance = ? 
                                                 WHERE account_no = ?";
      jdbcTemplate.update(updateQuery, new Object[]{newBalance, accountNo});
  }
}

我们使用事件应用点的余额和导致事件的金额来计算帐户的当前状态。

通过这些更改,我们完成了将事件溯源引入系统的过程。

如果您现在启动应用程序,您仍然应该看到相同的index页面,仍然可以信用/借记这两个帐户中的任何一个并反映其当前状态。

到目前为止,我们已经能够组装CQRS架构。 但是我们遗漏了任何软件应用程序的关键部分,那就是测试。

下一篇文章:使用Axon Framework探索CQRS:测试基础架构概述介绍了Axon提供的测试基础架构。

猜你喜欢

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