'Axon Aggregate Identifier Type Converter
PROBLEM: In an aggregate-state Axon SpringBoot application @AggregateIdentifier
is of type UUID
and PostgreSQL database column is of type UUID
. When persisted on create-commands - identifier is successfully stored. When sending update-command warning rises and command is not delivered to aggregate handler, because @TargetAggregateIdentifier
expects String type as described by Axon here:
java.lang.IllegalArgumentException: Provided id of the wrong type for class ....MyAggregate. Expected: class java.util.UUID, got class java.lang.String
RESEARCH:
Working solution is to refactor the domain to
String
types for all aggregate indentifiers. Database@Id
filed also should be converted tovarchar(36)
type to storeUUID.toString()
as primary key. Major minus is that it's inefficient: 9 times bigger size and slower String reads.Minimal boilerplate and compromise solution is to refactor the domain to
String
types for all aggregate indentifiers and usejavax.persistence.Converters
to convert String to UUID in JPA layer while persisting:@Converter public class UuidJpaConverter implements AttributeConverter<String, UUID> { @Override public UUID convertToDatabaseColumn(String uuid) { return fromString(uuid); } @Override public String convertToEntityAttribute(UUID uuid) { return uuid.toString(); } } ... @Aggregate @Entity @IdClass(UuidKey.class) public class MyAggregate implements Serializable { @AggregateIdentifier @Id private String uuid; ... } public class UuidKey implements Serializable { @Column(name = "uuid", nullable = false, updatable = false) @Convert(converter = UuidJpaConverter.class) private String uuid; }
But it results in:
o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: column "uuid" is of type uuid but expression is of type bytea
Use Axon identifier converter, but it requires custom GenericJpaRepository for each aggregate and actually never calls identifierConverter handler on a breakpoint:
@Configuration public class IdentifierConverter { @Bean public GenericJpaRepository<MyAggregate> aggregateJpaRepository( EntityManagerProvider provider, @Qualifier("eventBus") EventBus simpleEventBus) { GenericJpaRepository<MyAggregate> repository = GenericJpaRepository .builder(MyAggregate.class) -> .identifierConverter(name -> UUID.fromString(name)) .entityManagerProvider(provider) .eventBus(simpleEventBus) .build(); return repository; } }
And results in:
o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: column "uuid" is of type uuid but expression is of type character varying
There is also a suggested universal GenericJpaRepository solution to find the type of the aggregate identifier and - unless it is already a String - convert it via the Spring conversion service. But it is not clear where to bind it added it to the
registerAggregateBeanDefinitions
in the configurer when we have Axon autoconfiguration and beans are expected in favour of Configurer:final Class<?> aggregateIdentifierType = Stream.of( aggregateType.getDeclaredFields( ) ) .filter( field -> field.isAnnotationPresent( AggregateIdentifier.class ) ) .map( field -> field.getType( ) ) .findFirst( ) .orElseThrow( ( ) -> new IllegalStateException( "The aggregate '" + aggregate + "' does not have an identifier." ) ); aggregateConf.configureRepository( c -> GenericJpaRepository.builder( aggregateType ) .identifierConverter( string -> { if ( aggregateType == String.class ) { return string; } else { try { final ConversionService conversionService = beanFactory.getBean( ConversionService.class ); return conversionService.convert( string, aggregateIdentifierType ); } catch ( final NoSuchBeanDefinitionException ex ) { throw new IllegalStateException( "Unable to convert String to aggregate identifier of type '" + aggregateIdentifierType.getName( ) + "'. A conversion service is missing." ); } } } ) ... @Named final class MyIdConverter implements Converter<String, MyId> { ... @Override public MyId convert( final String source ) { return MyId.fromString( source ); } }
QUESTION: How to keep UUID type of aggregate identifier in PostgreSQL database - with UUID
as preferred type for @AggregateIdentifier
or at least String
. Also why is only String
currently supported if at 2010 UUID was widely used in Axon?
Solution 1:[1]
Honestly, you've gone pretty deep on the matter here @Zon. Unsure whether I can help you sufficiently, but I will give it a go regardless.
Research point 1 is obviously the most pragmatic solution to get things to work right now. If you are going to notice the "ineffeciency" of String
compared to UUID
is something I'd be hard pressed about. So if this is an absolute no no, investigation should proceed. Otherwise, it does get the job done of course.
When it comes to research points 2 and 3, I believe you are hitting an issue with the dialect being used for PostgreSQL, although I am not a 100% certain here. Especially PostgreSQL brandishes a couple of "awesome" types, but these do not always automatically work in all scenarios. I am basing my "guesswork" here on forcing PostgreSQL to use BYTEA
instead of OID
in case you want to should down Postgres' TOAST capability. This becomes especially handy if you opt to use Postgres for your event store and want to be able to actually see the contents of events. This blog post for example specifies how to deal with this. More importantly, this blog post shows how you could for example adjust the dialect being used. Maybe that could serve you in solutions 2 and 3?
Option 4 should in this case be the most logical solution to take. But I gather from your response that you didn't get it to work at the moment. When combing Axon with Spring, the SpringAxonAutoConfigurer
(from which you are referring the registerAggregateBeanDefinitions
method from I believe) will automatically check for configurable beans on your Aggregate. It does so based on fields defined in the @Aggregate
(i.e. Axon's Spring stereotype annotation). More specifically, you can use the repository
field in the @Aggregate
to define the bean name of the repository you want to use.
You should thus simply be able to provide a GenericJpaRepository
bean with the desired identifierConverter
. The name of that bean can than be specified in the @Aggregate
annotation on your MyAggregate
, so that Axon's auto config can pick it up correctly. Hope this helps you out!
Solution 2:[2]
Here is a working solution for option #3, creating a custom GenericJpaRepository for each aggregate. Similar how Spring does a component scan, this will scan for classes with @Aggregate
from a base package, which can either be defined in your properties file or replace in this config class with a literal.
@Configuration
public class GenericJpaRepositoryConfig {
@Value("${base-package}")
private String basePackage;
@Autowired
public void registerGenericJpaRepositories(ApplicationContext applicationContext,
EntityManagerProvider provider,
@Qualifier("eventBus") EventBus simpleEventBus) throws ClassNotFoundException {
List<Class<?>> classes = this.getAggregateClasses();
ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
for (Class<?> clazz : classes) {
GenericJpaRepository<?> repository = aggregateJpaRepository(provider, simpleEventBus, clazz);
beanFactory.registerSingleton("axon" + clazz.getSimpleName() + "Repository", repository);
}
}
private <T> GenericJpaRepository<T> aggregateJpaRepository(
EntityManagerProvider provider,
EventBus simpleEventBus,
Class<T> className) {
return GenericJpaRepository
.builder(className)
.identifierConverter(UUID::fromString)
.entityManagerProvider(provider)
.eventBus(simpleEventBus)
.build();
}
private List<Class<?>> getAggregateClasses() throws ClassNotFoundException {
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(true);
List<Class<?>> classes = new ArrayList<>();
if (basePackage == null || basePackage.isEmpty()) {
basePackage = this.getClass().getPackageName();
}
for (BeanDefinition bd : scanner.findCandidateComponents(basePackage)) {
Class<?> clazz = Class.forName(bd.getBeanClassName());
Aggregate annotation = clazz.getAnnotation(Aggregate.class);
if (annotation != null) {
classes.add(clazz);
}
}
return classes;
}
}
Note that each created GenericJpaRepository has a name of the format axon<AggregateName>Repository
. To get this to work with your aggregate class that you named MyAggregate
you will need the annotation with the repository value: @Aggregate(repository = "axonMyAggregateRepository")
. Adding the repository name to the aggregate annotation was mentioned in Steven's answer, and is probably why your solution didn't work.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Steven |
Solution 2 | Michael Jeszenka |