TRIMM 1.0.1 Snapshot is available with support for CQRS and AxonFramework

TRIMM 1.0.1 Snapshot is available with support for CQRS and AxonFramework

* Updated the 25th of October to include information on CreateCommand vs. Command stereotype handling for the TrimmCQRS plugin.

The first Maven snapshot of TRIMM 1.0.1 is available from our Maven Repository.

The changes in the core API revolve around enhancements the MetaModel and CodeModel, no breaking API changes.

The biggest change is the addition of the TrimmCQRS which supports Model Driven code generating of AxonFramework (a Java based CQRS framework) based CQRS code.

TrimmCQRS comes with a matching TrimmCQRSMavenPlugin that can be used to drive the codegeneration.

There’s also an TrimmCQRS example project available called TrimmCQRSAxonAddressBookExample which models and generates code that implement the same features and structure of the Axon AddressBook example.

Short introduction to TrimmCQRS modeling

Most people that have implemented a CQRS based application in a traditional OO language are familiar with the slightly repetitive nature of how the code in CommandHandlers, Aggregates and EventsHandlers.

In this short introduction I’m going to show how we modeled the Axon AddressBook example as part of the TrimmCQRSAxonAddressBookExample.

The modeling structure follows the AddressBook example:

TrimmCQRS AddressBook Project Model structure

TrimmCQRS AddressBook Project Model structure

The API package

The API package contains the public API for the AddressBook. It contains the Commands and Events, plus the ValueObjects/Enumerations (Address and AddressType) that are referenced from the Commands and Events.

The Command package

The Command package contains the Contact aggregate and the ContactCommandHandler

In the API package I’ve created a diagram that brings together the classes and interfaces from the api and command packages.

TrimmCQRS AddressBook API model

TrimmCQRS AddressBook API model

The Query package

The Query package contains the AddressTableUpdater which listens to Events and updates and underlying Query/View model.
I’ve only modeled the CQRS part of the solution and not the underlying JPA based model and Repository that the AddressTableUpdater is using (this could be done using TrimmJPA)

TrimmCQRS AddressBook Query model

TrimmCQRS AddressBook Query model

Explanations for the model elements

  • Contact Aggregate Root – is marked with the AggregateRoot stereotype. In this simple case the identifier is an attribute in the Contact class, which get marked with the Identifier stereotype.
  • Events – All the Events signal state changes in the Contact aggregate root. All Events are marked with the Event stereotype. If you notice we have two Events AddressAddedEvent and AddressChangedEvent which both don’t have a Event stereotype. They don’t need this since they both inherit from the abstract AddressRegistredEvent class which has the Event stereotype (and any shared properties that both concrete events share).

    Event are associated with their aggregate root using composite association (filled diamond shape). The reason for choosing a composite association is to signal that the Aggregate’s state is composed of all its events.
    Note: The composite associations between the Aggregate and its Events of course doesn’t result in a 1-1 association properties being created for each Event in the Aggregate, like it would if you used regular TrimmJava/TrimmJpa/TrimmGroovy code generation.

    The Events doesn’t carry the identifier of the Aggregate since that would be redundant. We already know the Events are related to the aggregate, so the TrimmCQRS generator will ensure that the Aggregate identifier is added as a readonly/immutable property to all Events.

    Events with inheritance hierarchies
    When you have Events that are part of a hierarchy you have the option of choosing two different way of creating an association from the AggregateRoot (or EventHandler).

    Either you create a composite association to the one of the generic/abstract events (like the association between the Contact aggregate root and the AddressRegistredEvent event). By doing this you indicate to TrimmCQRS that you only want one EventHandling/Event replaying method for the abstract/generic event generated into the AggregateRoot code. This allows you to, in this case, handle both AddressAddedEvent and AddressChangedEvent using one AggregateRoot event handler method which accepts AddressRegistredEvent.

    The other option is to create an association to each of the specific events (AddressAddedEvent and AddressChangedEvent) which is what’s done in the query package, where the AddressTableUpdater handles AddressAddedEvent and AddressChangedEvent using two specific eventhandler methods.

  • The ContactCommandHandler interface is marked with the CommandHandler stereotype, which indicates to the TrimmCQRS generator that the Commands that are associated using shared/aggregated association (hollow diamond) each will become commandhandler methods inside the generated CommandHandler interface. The CommandHandler is associated to the AggregateRoot that it acts upon using a directed simple association.
  • Commands – are marked with Command or CreateCommand stereotypes. Like for the Events, the modeled commands don’t carry the identifier of the Aggregate since that again would be redundant. We already know the Commands are related to the aggregate (through the ContactCommandHandler‘s directed association to the Contact aggregate root, so the TrimmCQRS generator will ensure that the Aggregate identifier is added as a readonly/immutable property to all Commands marked with Command.
    The CreateCommand will not get the aggregate identifier copied since they don’t always need/want the id to be part of the creation. There’s typically two ways of handling Create commands. Either they don’t include the aggregate identifier and get one assigned by the CommandHandler or they do include the aggregate identifier (aka. client specified id), in which case you can include it manually or chose to mark the command with the Command stereotype instead.
  • The AddressTableUpdater EventHandler interface is marked with the EventHandler stereotype. The EventHandler is associated with the Events it listens to using composite association (filled diamond shape).
    Note: The composite associations between the EventHandler and its Events of course doesn’t result in a 1-1 association properties being created for each Event in the Aggregate, like it would if you used regular TrimmJava/TrimmJpa/TrimmGroovy code generation.
    Instead the associations to the events result in a EventHandler methods to be generated.
  • The Address ValueObject are marked with the ValueObject stereotype. This ensures that the ValueObject becomes immutable.

Short introduction to TrimmCQRS codegeneration

Code generation for a TrimmCQRS annotated UML class model can be done in a classical Maven project by using the TrimmCQRSMavenPlugin.
For the AddressBook example you need to XMI Export the addressbook package from Enterprise Architect (as XMI version 2.1) to the Java example projects model/ subfolder under the name AddressBook.xml (since this is how we reference it in the CQRSConfiguration.yml that I will cover in a second)
See here for information about modeling in Enterprise Architect in general and here for information about Exporting a model to XMI 2.1.

Next we need to add the TrimmCQRSMavenPlugin to the Java example projects pom.xml:

<plugin>
	<groupId>dk.tigerteam</groupId>
	<artifactId>TrimmCQRSMavenPlugin</artifactId>
	<version>1.0.1-SNAPSHOT</version>
	<executions>
		<execution>
			<id>generate</id>
			<phase>generate-sources</phase>
			<goals>
				<goal>generate</goal>
			</goals>
			<configuration>
				<yamlFile>model/CQRSConfiguration.yml</yamlFile>
			</configuration>
		</execution>
	</executions>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>2.6</version>
		</dependency>
	</dependencies>
</plugin>

In the CQRSConfiguration.yml YAML based configuration file (which is also placed in the model/ subfolder) we specify the generator configuration:

# The path to the XMI model file. e.g. model/MyModel.xml
xmiModelPath: model/AddressBook.xml
# The UML tool used. Example: EA, MagicDraw16, MagicDraw17 or FQCN/.groovy script (implementing the XmiReader interface)
umlTool: EA
# The optional base package name that should be appended to all generated code. Example: dk.tigerteam.myproject
basePackageName: dk.tigerteam.cqrs.axon

# Paths
# The required path where base-classes (classes generated each time) will generated to. 
generateBaseClassesToPath: target/generated-sources/model
# The required path where extension-classes (classes generated only once and only if missing) will generated to.
generateExtensionClassesToPath: src/main/model-extensions
# The path where interfaces (similar to base-classes, generated every time) will generated to.
generateInterfacesToPath: target/generated-sources/model

Of course we also need to add a dependency to axon-core (at least) and commons-lang (since the code generated by TrimmCQRS uses commons-lang in the generated equals/hashCode methods) in the pom.xml

<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-core</artifactId>
    <version>2.0.5</version>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

To generate the code you only need to execute the following maven command:
mvn generate-sources

The following code is getting generated:

TrimmCQRS AddressBook example generated code structure

TrimmCQRS AddressBook example generated code structure

In the target/generated-sources/model will be regenerated everytime (ofcourse only when the model or configuration has changed). The code in src/main/model-extensions will only be generated once (if there isn’t already a matching file by the same name). This follows the pattern on Generator Gap pattern / Extension Classes / 3 level inheritance.

The code generation follows patterns, so I will only show one example of each:

The Abstract Contact Aggregate Root

Since the Aggergate root will contain business logic which is why we generate both an abstract version called AbstractContact and a (one time only) concrete version called Contact.

AbstractContact.java from target/generated-sources/model

package dk.tigerteam.cqrs.axon.addressbook.command;

import dk.tigerteam.cqrs.axon.addressbook.api.AddressRegisteredEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.AddressRemovedEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.ContactCreatedEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.ContactDeletedEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.ContactNameChangedEvent;

import org.axonframework.eventhandling.annotation.EventHandler;

import org.axonframework.eventsourcing.annotation.AbstractAnnotatedAggregateRoot;
import org.axonframework.eventsourcing.annotation.AggregateIdentifier;


abstract public class AbstractContact extends AbstractAnnotatedAggregateRoot<String> {
    @AggregateIdentifier
    protected String id;

    @SuppressWarnings("UnusedDeclaration")
    protected AbstractContact() {
    }

    public AbstractContact(String id) {
        super();
        this.id = id;
    }

    @EventHandler
    protected abstract void handle(AddressRegisteredEvent event);

    @EventHandler
    protected abstract void handle(AddressRemovedEvent event);

    @EventHandler
    protected abstract void handle(ContactCreatedEvent event);

    @EventHandler
    protected abstract void handle(ContactDeletedEvent event);

    @EventHandler
    protected abstract void handle(ContactNameChangedEvent event);

    public String toString() {
        return "AbstractContact[" + "id: " + id + "]";
    }
}

Contact.java from src/main/model-extensions

public class Contact extends AbstractContact {
    public Contact(String id) {
        super(id);
    }
}

Events

Here I will show the abstract AddressRegisteredEvent and one of the concrete events AddressChangedEvent

AddressRegisteredEvent.java from target/generated-sources/model

package dk.tigerteam.cqrs.axon.addressbook.api;

import dk.tigerteam.cqrs.axon.addressbook.api.Address;
import dk.tigerteam.cqrs.axon.addressbook.api.AddressType;

import java.io.Serializable;


public abstract class AddressRegisteredEvent implements Serializable {
    private Address address;
    private AddressType type;
    private String contactId;

    public AddressRegisteredEvent(Address address, String contactId,
        AddressType type) {
        super();
        this.address = address;
        this.contactId = contactId;
        this.type = type;
    }

    public Address getAddress() {
        return address;
    }

    public AddressType getType() {
        return type;
    }

    public String getContactId() {
        return contactId;
    }

    @Override
    public boolean equals(Object rhs) {
        return org.apache.commons.lang.builder.EqualsBuilder.reflectionEquals(this,
            rhs, false, AddressRegisteredEvent.class, new String[] {  });
    }

    @Override
    public int hashCode() {
        return org.apache.commons.lang.builder.HashCodeBuilder.reflectionHashCode(17,
            31, this, false, AddressRegisteredEvent.class, new String[] {  });
    }

    public String toString() {
        return "AddressRegisteredEvent[" + "address: " + address +
        ", contactId: " + contactId + ", type: " + type + "]";
    }
}

AddressChangedEvent.java from target/generated-sources/model

package dk.tigerteam.cqrs.axon.addressbook.api;

import dk.tigerteam.cqrs.axon.addressbook.api.Address;
import dk.tigerteam.cqrs.axon.addressbook.api.AddressRegisteredEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.AddressType;

import java.io.Serializable;


public class AddressChangedEvent extends AddressRegisteredEvent
    implements Serializable {
    public AddressChangedEvent(Address address, String contactId,
        AddressType type) {
        super(address, contactId, type);
    }

    @Override
    public boolean equals(Object rhs) {
        return org.apache.commons.lang.builder.EqualsBuilder.reflectionEquals(this,
            rhs, false, AddressChangedEvent.class, new String[] {  });
    }

    @Override
    public int hashCode() {
        return org.apache.commons.lang.builder.HashCodeBuilder.reflectionHashCode(17,
            31, this, false, AddressChangedEvent.class, new String[] {  });
    }
}

Commands

CreateContactCommand.java from target/generated-sources/model
This Command is marked with the CreateCommand stereotype so it doesn’t carry the Contact aggregate id

package dk.tigerteam.cqrs.axon.addressbook.api;

public class CreateContactCommand {
    private String newContactName;

    public CreateContactCommand(String newContactName) {
        super();
        this.newContactName = newContactName;
    }

    public String getNewContactName() {
        return newContactName;
    }

    @Override
    public boolean equals(Object rhs) {
        return org.apache.commons.lang.builder.EqualsBuilder.reflectionEquals(this,
            rhs, false, CreateContactCommand.class, new String[] {  });
    }

    @Override
    public int hashCode() {
        return org.apache.commons.lang.builder.HashCodeBuilder.reflectionHashCode(17,
            31, this, false, CreateContactCommand.class, new String[] {  });
    }

    public String toString() {
        return "CreateContactCommand[" + "newContactName: " + newContactName +
        "]";
    }
}

ChangeContactNameCommand.java from target/generated-sources/model
This Command is marked with the Command stereotype and therefore carries the Contact aggregate id

package dk.tigerteam.cqrs.axon.addressbook.api;

import java.io.Serializable;


public class ChangeContactNameCommand implements Serializable {
    private String contactNewName;
    private String contactId;

    public ChangeContactNameCommand(String contactId, String contactNewName) {
        super();
        this.contactId = contactId;
        this.contactNewName = contactNewName;
    }

    public String getContactNewName() {
        return contactNewName;
    }

    public String getContactId() {
        return contactId;
    }

    @Override
    public boolean equals(Object rhs) {
        return org.apache.commons.lang.builder.EqualsBuilder.reflectionEquals(this,
            rhs, false, ChangeContactNameCommand.class, new String[] {  });
    }

    @Override
    public int hashCode() {
        return org.apache.commons.lang.builder.HashCodeBuilder.reflectionHashCode(17,
            31, this, false, ChangeContactNameCommand.class, new String[] {  });
    }

    public String toString() {
        return "ChangeContactNameCommand[" + "contactId: " + contactId +
        ", contactNewName: " + contactNewName + "]";
    }
}

Contact CommandHandler

ContactCommandHandler.java from target/generated-sources/model

package dk.tigerteam.cqrs.axon.addressbook.command;

import dk.tigerteam.cqrs.axon.addressbook.api.ChangeContactNameCommand;
import dk.tigerteam.cqrs.axon.addressbook.api.CreateContactCommand;
import dk.tigerteam.cqrs.axon.addressbook.api.RegisterAddressCommand;
import dk.tigerteam.cqrs.axon.addressbook.api.RemoveAddressCommand;
import dk.tigerteam.cqrs.axon.addressbook.api.RemoveContactCommand;

import org.axonframework.commandhandling.annotation.CommandHandler;

import org.axonframework.unitofwork.UnitOfWork;


public interface ContactCommandHandler {
    @CommandHandler
    void handle(ChangeContactNameCommand cmd, UnitOfWork unitOfWork);

    @CommandHandler
    void handle(CreateContactCommand cmd, UnitOfWork unitOfWork);

    @CommandHandler
    void handle(RegisterAddressCommand cmd, UnitOfWork unitOfWork);

    @CommandHandler
    void handle(RemoveAddressCommand cmd, UnitOfWork unitOfWork);

    @CommandHandler
    void handle(RemoveContactCommand cmd, UnitOfWork unitOfWork);
}

AddressTableUpdater EventListener

AddressTableUpdater.java from target/generated-sources/model

package dk.tigerteam.cqrs.axon.addressbook.query;

import dk.tigerteam.cqrs.axon.addressbook.api.AddressAddedEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.AddressChangedEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.AddressRemovedEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.ContactCreatedEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.ContactDeletedEvent;
import dk.tigerteam.cqrs.axon.addressbook.api.ContactNameChangedEvent;

import org.axonframework.eventhandling.annotation.EventHandler;


public interface AddressTableUpdater {
    @EventHandler
    void handle(AddressAddedEvent event);

    @EventHandler
    void handle(AddressChangedEvent event);

    @EventHandler
    void handle(AddressRemovedEvent event);

    @EventHandler
    void handle(ContactCreatedEvent event);

    @EventHandler
    void handle(ContactDeletedEvent event);

    @EventHandler
    void handle(ContactNameChangedEvent event);
}

Adress valueobject

Address.java from target/generated-sources/model

package dk.tigerteam.cqrs.axon.addressbook.api;

import java.io.Serializable;


public class Address implements Serializable {
    private String city;
    private String streetAndNumber;
    private String zipCode;

    public Address(String city, String streetAndNumber, String zipCode) {
        super();
        this.city = city;
        this.streetAndNumber = streetAndNumber;
        this.zipCode = zipCode;
    }

    public String getCity() {
        return city;
    }

    public String getStreetAndNumber() {
        return streetAndNumber;
    }

    public String getZipCode() {
        return zipCode;
    }

    @Override
    public boolean equals(Object rhs) {
        return org.apache.commons.lang.builder.EqualsBuilder.reflectionEquals(this,
            rhs, false, Address.class, new String[] {  });
    }

    @Override
    public int hashCode() {
        return org.apache.commons.lang.builder.HashCodeBuilder.reflectionHashCode(17,
            31, this, false, Address.class, new String[] {  });
    }

    public String toString() {
        return "Address[" + "city: " + city + ", streetAndNumber: " +
        streetAndNumber + ", zipCode: " + zipCode + "]";
    }
}

That’s it for now. If you have feedback on the modeling, code-generation, new ideas, we’d love to hear them.

Enjoy 🙂

 
Comments

Hello is there any route map for this project?

hi is there any mdg file for cqrs based project