Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch Command / Multi Command Container for single Aggregate #991

Closed
benneq opened this issue Feb 15, 2019 · 2 comments
Closed

Batch Command / Multi Command Container for single Aggregate #991

benneq opened this issue Feb 15, 2019 · 2 comments
Assignees
Labels
Type: Question Use to signal the issue is a question of how the project works and thus does not require development

Comments

@benneq
Copy link

benneq commented Feb 15, 2019

For our API it would be nice to have some kind of BatchCommand. This is basically a command, which contains a List of commands for a single Aggregate.

If you have fine granular commands (and no cache), this would provide some huge performance improvements, because the aggregate must only be loaded once. And the resulting events can be persisted with a single write operation.

At the moment I use a custom MyAggregateBatchCommand, which gets handled by MyAggregate and this then calls the matching @CommandHandlers on this Aggregate. Something like this:

@Aggregate
class MyAggregate {
  @CommandHandler
  public void handle(FooCommand cmd) {
    // validate cmd
    apply(new FooEvent(...));
  }

  @CommandHandler
  public void handle(BarCommand cmd) {
    // validate cmd
    apply(new BarEvent(...));
  }

  @CommandHandler
  public void handle(MyAggregateBatchCommand cmd) {
    // pseudo code, because shorter:
    cmd.getCommands().forEach(command -> {
      command.setTargetAggregateIdentifier(cmd.targetAggregateIdentifier);
      command.setTargetAggregateVersion(cmd.targetAggregateVersion++);
      this.handle(command);
    });
  }
}

But this has a few issues:

  1. If a @CommandHandler requires additional parameters like @MetaData or some Spring Bean injection, the MyAggregateBatchCommand command handler will become really huge and ugly.
  2. External Command Handlers can only work, if you inject those into the MyAggregateBatchCommand command handler, which makes the code even more coupled, and ugly.
  3. (here I'm not sure, if this is intended or just a lucky coincidence) If one of the command handlers throws an exception, then all previously applied events are discarded. That's nice. But I'm not sure if this is "by design".

As far as I've seen CommandBus and CommandGateway only provide the execution of a single command. This makes sense, because each command can contain a different @TargetAggregateIdentifier. Though I guess, I won't make sense to add a dispatchBatch or sendBatch method.

Basically all I need is to dispatch more commands from the MyAggregateBatchCommand command handler, and stay within the same UnitOfWork (as far as I understood the concept) in order to have a "all or nothing" transaction.

So my question is: How can this be achieved?

@benneq benneq changed the title Batch Command / Command Container for single Aggregate Batch Command / Multi Command Container for single Aggregate Feb 15, 2019
@benneq
Copy link
Author

benneq commented Feb 15, 2019

I maybe found a solution, but I'm not sure at all if this behavior is intended.

Some feedback would be highly appreciated!

@Aggregate
class MyAggregate {
  @CommandHandler
  public void handle(FooCommand cmd) {
    // validate cmd
    apply(new FooEvent(...));
  }

  @CommandHandler
  public void handle(BarCommand cmd) {
    if("1".equals("1")) { // simulate validation exception
      throw new RuntimeException("??");
    }
    apply(new BarEvent(...));
  }

  @CommandHandler
  public void handle(
      MyAggregateBatchCommand cmd,
      CommandGateway commandGateway,
      UnitOfWork<?> unitOfWork) {
    
    try {
      FooCommand cmd1 = cmd.getFooCommand();
      cmd1.setTargetAggregateIdentifier(cmd.getTargetAggregateIdentifier());
      cmd1.setTargetAggregateVersion(cmd.getTargetAggregateVersion());
      commandGateway.sendAndWait(cmd1); // important to use "andWait" !

      BarCommand cmd2 = cmd.getBarCommand();
      cmd2.setTargetAggregateIdentifier(cmd.getTargetAggregateIdentifier());
      cmd2.setTargetAggregateVersion(cmd.getTargetAggregateVersion() + 1);
      commandGateway.sendAndWait(cmd2); // important to use "andWait" !
    } catch(Exception ex) {
      unitOfWork.rollback(ex);
    }

  }
}

This works as expected. I guess Axon has some kind of UnitOfWork hierarchy, because all 3 @CommandHandlers receive different UnitOfWork instances. But the unitOfWork.rollback() call on the "parent" discards all generated events.

If I remove the try-catch block, then FooEvent will get persisted.


Is this the way to go? Or what can go wrong? Will it load the Aggregate only once? (I'm not sure how Axon internally handles this)

@smcvb smcvb added Type: Feature Use to signal an issue is completely new to the project. Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. labels Mar 14, 2019
@abuijze
Copy link
Member

abuijze commented Aug 7, 2019

Batch commands are usually best handled in an "external Command Handler". I.e. a Command Handler that is not inside an Aggregate, but rather on a "singleton" bean.
Explicitly rolling back the Unit of Work is not recommended. Instead, the command handler should take the necessary actions to ensure side-effects that have been executed already are rolled back.

Since this seems to be a question, rather than an issue, I'm closing it here. You can refer to the mailinglist (axonframework@googlegroups.com) for questions, or comment here if you feel this is an issue.

@abuijze abuijze closed this as completed Aug 7, 2019
@abuijze abuijze added Type: Question Use to signal the issue is a question of how the project works and thus does not require development and removed Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. Type: Feature Use to signal an issue is completely new to the project. labels Aug 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Question Use to signal the issue is a question of how the project works and thus does not require development
Projects
None yet
Development

No branches or pull requests

3 participants