1

我对如何将数据同步到查询数据库感到困惑。

假设我有一个聚合:CreditAccount和一些命令可能会产生CreditAccountBalanceChangedEvent

public class CreditAccount extends AbstractAnnotatedAggregateRoot<Long> {

    @AggregateIdentifier
    private Long id;
    private int balance;
    private DateRange effectiveDateRange;

    @CommandHandler
    public CreditAccount(CreateCreditAccountCommand command) {
        apply(new CreditAccountCreatedEvent(command.getAccountId(),
            command.getEffectiveDateRange()));
        apply(new CreditAccountBalanceChangedEvent(command.getAccountId(),
            command.getAmount()));
    }

    @EventHandler
    private void on(CreditAccountCreatedEvent event) {
        this.id = event.getAccountId();
        this.effectiveDateRange = event.getEffectiveDateRange();
    }

    @EventHandler
    private void on(CreditAccountBalanceChangedEvent event) {
        //notice this line, some domain logic here 
        this.balance = add(this.balance, event.getAmount());
    }

    private int add(int current, int amount) {
        return current + amount;
    }
}

public class CreditAccountBalanceChangedEvent {
    private final long accountId;
    private final int amount;
    //omitted constructors and getters
}

在命令处理程序方面一切正常。然后我开始查询,但我发现我在这里写了一些重复的域逻辑:

@Transactional
@Slf4j
public class CreditAccountEventHandler {

    private CreditAccountReadModelStore creditAccountReadModelStore;

    @EventHandler
    public void handle(CreditAccountCreatedEvent event) {
        log.info("Received " + event);
        creditAccountReadModelStore.store(accountDevriveFrom(event));
    }

    @EventHandler
    public void handle(CreditAccountBalanceChangedEvent event) {
        log.info("Received " + event);
        final CreditAccountReadModel account = creditAccountReadModelStore
            .findBy(event.getAccountId());
        //notice this line, some domain logic here 
        account.setBalance(account.getBalance() + event.getAmount());
        creditAccountReadModelStore.store(account);
    }
    //omitted setters and private methods
}

你可能注意到了,我在命令端和查询端都写了余额计算代码。我的问题是这在某些情况下是不可避免的还是我在错误的地方编写了域逻辑?

到目前为止,我的研究表明,事件代表了某些事情已经发生,因此它们中没有业务逻辑,它们只是数据持有者(但揭示了用户的意图)。那么我应该向CreditAccountBalanceChangedEvent添加一个“余额”字段并将余额计算代码移动到命令处理程序方法吗?

public class CreditAccount extends AbstractAnnotatedAggregateRoot<Long> {

    //omitted fields

    @CommandHandler
    public CreditAccount(CreateCreditAccountCommand command) {
        apply(new CreditAccountCreatedEvent(command.getAccountId(),
            command.getEffectiveDateRange()));
        apply(new CreditAccountBalanceChangedEvent(command.getAccountId(),
            command.getAmount(), add(this.balance, command.getAmount())));
    }        


    @EventHandler
    private void on(CreditAccountBalanceChangedEvent event) {
        //notice this line, some domain logic here
        //event.getAmount() is no use here, just for auditing? 
        this.balance = event.getBalance();
    }

}

在这种情况下,我可以使用 event.getBalance() 删除查询端的余额计算。

很抱歉出现全屏问题,任何想法都值得赞赏。

4

1 回答 1

1

我看到两个选项。

一种是包含余额变化的命令,计算新余额的命令处理程序,以及包含新余额的事件。如果在事件处理程序中没有重新计算任何内容,它可以确保如果未来业务规则发生变化,当从事件中重构对象时,它们不会影响对象的历史记录。

另一种方法是将业务规则放在一个单独的类中,该类从命令处理程序和事件处理程序都调用以避免重复,然后对这些业务规则进行版本化——例如通过子类化。因此,您可以拥有一个名为CalculateBalanceRule 的抽象类,其中包含一个CalculateBalanceRuleVersion1 的子类,最初由两者引用。如果规则发生更改,则创建 CalculateBalanceRuleVersion2,更改命令处理程序以引用它,但在事件处理程序中保留对 Version1 的引用,以便它始终重播它最初执行的规则。

第二种方法肯定是更多的维护,但如果这对您的业务很重要,则可以回答如何改变,而不仅仅是改变了什么。

编辑:第三个选项是让事件只包含第一个选项中的新平衡,但对事件进行版本化。所以你有 BalanceChangedEvent、BalanceChangedEvent_v2 等等。这是我可以采取的方向,因为我并不真正关心记录事情如何变化的历史,但我确实需要考虑事件本身可能会吸引更多成员或重命名其成员的可能性。然后需要逻辑来确定在每个步骤中使用哪个事件版本来重构对象。

于 2013-09-11T19:54:24.973 回答