question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

State-Stored Aggregate: AggregateIdentifier has to be String

See original GitHub issue

Hi everyone,

we use state-based aggregates in our application.

We encountered that we have to use String for the aggregate identifier. Otherwise an exception occurs while sending a command to the aggregate by annotating the id field with @TargetAggregateIdentifier. In this example I tried to use a java.util.UUID:

java.lang.IllegalStateException: Failed to execute CommandLineRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:816) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:797) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:324) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:139) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at de.libutzki.axon.axonhierarchical.AxonHierarchicalApplication.main(AxonHierarchicalApplication.java:20) ~[classes/:na]
Caused by: java.lang.IllegalArgumentException: Provided id of the wrong type for class de.libutzki.axon.axonhierarchical.module2.entity.Module2Aggregate. Expected: class java.util.UUID, got class java.lang.String
	at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3503) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3456) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:308) ~[spring-orm-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at com.sun.proxy.$Proxy71.find(Unknown Source) ~[na:na]
	at org.axonframework.modelling.command.GenericJpaRepository.doLoadWithLock(GenericJpaRepository.java:110) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.modelling.command.GenericJpaRepository.doLoadWithLock(GenericJpaRepository.java:53) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.modelling.command.LockingRepository.doLoad(LockingRepository.java:118) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.modelling.command.LockingRepository.doLoad(LockingRepository.java:52) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.modelling.command.AbstractRepository.lambda$load$4(AbstractRepository.java:122) ~[axon-modelling-4.1.1.jar:4.1.1]
	at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1133) ~[na:na]
	at org.axonframework.modelling.command.AbstractRepository.load(AbstractRepository.java:121) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.modelling.command.AggregateAnnotationCommandHandler$AggregateCommandHandler.handle(AggregateAnnotationCommandHandler.java:349) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.modelling.command.AggregateAnnotationCommandHandler$AggregateCommandHandler.handle(AggregateAnnotationCommandHandler.java:337) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.modelling.command.AggregateAnnotationCommandHandler.handle(AggregateAnnotationCommandHandler.java:125) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.modelling.command.AggregateAnnotationCommandHandler.handle(AggregateAnnotationCommandHandler.java:44) ~[axon-modelling-4.1.1.jar:4.1.1]
	at org.axonframework.messaging.DefaultInterceptorChain.proceed(DefaultInterceptorChain.java:57) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.messaging.interceptors.CorrelationDataInterceptor.handle(CorrelationDataInterceptor.java:65) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.messaging.DefaultInterceptorChain.proceed(DefaultInterceptorChain.java:55) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.messaging.unitofwork.DefaultUnitOfWork.executeWithResult(DefaultUnitOfWork.java:74) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.commandhandling.SimpleCommandBus.handle(SimpleCommandBus.java:176) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.commandhandling.SimpleCommandBus.doDispatch(SimpleCommandBus.java:141) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.commandhandling.SimpleCommandBus.dispatch(SimpleCommandBus.java:110) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.commandhandling.gateway.AbstractCommandGateway.send(AbstractCommandGateway.java:75) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.commandhandling.gateway.DefaultCommandGateway.send(DefaultCommandGateway.java:73) ~[axon-messaging-4.1.1.jar:4.1.1]
	at org.axonframework.commandhandling.gateway.DefaultCommandGateway.sendAndWait(DefaultCommandGateway.java:90) ~[axon-messaging-4.1.1.jar:4.1.1]
	at de.libutzki.axon.axonhierarchical.module2.Module2Runner.run(Module2Runner.java:42) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294) ~[spring-tx-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) ~[spring-tx-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at com.sun.proxy.$Proxy85.run(Unknown Source) ~[na:na]
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:813) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	... 4 common frames omitted
Caused by: org.hibernate.TypeMismatchException: Provided id of the wrong type for class de.libutzki.axon.axonhierarchical.module2.entity.Module2Aggregate. Expected: class java.util.UUID, got class java.lang.String
	at org.hibernate.event.internal.DefaultLoadEventListener.checkIdClass(DefaultLoadEventListener.java:169) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1256) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	at org.hibernate.internal.SessionImpl.access$1900(SessionImpl.java:207) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	at org.hibernate.internal.SessionImpl$IdentifierLoadAccessImpl.doLoad(SessionImpl.java:2866) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	at org.hibernate.internal.SessionImpl$IdentifierLoadAccessImpl.load(SessionImpl.java:2847) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3482) ~[hibernate-core-5.3.9.Final.jar:5.3.9.Final]
	... 46 common frames omitted

While researching I stumbled upon org.axonframework.modelling.command.GenericJpaRepository.Builder.identifierConverter(Function<String, ?>)

But there are two major problems:

  1. I use Spring Boot and the GenericJpaRepositories are built in org.axonframework.spring.config.SpringAxonAutoConfigurer.registerAggregateBeanDefinitions(Configurer, BeanDefinitionRegistry) which cannot be customized easily. I have to override (nearly) the whole class to adjust the behaviour. Anyway, by doing this and setting .identifierConverter(UUID::fromString) the command can be handled.
  2. The solution only works if I use a type which’s toString() result can be reverted back to the type (like UUID.fromString(String) does). In practice I don’t want to use a UUID as id but a value object like CustomerId which wraps a UUID. Having this I do not see any chance to annotate it with @AggregateIdentifier and to use it in a state-based aggregate.

As stated in #484 you also prefer the usage of value objects, but unfortunately I do not find a way to do this. Maybe it’s a lack of documentation, but I fear it’s a missing feature. That’s the reason why I decided to create this issue.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:2
  • Comments:10 (9 by maintainers)

github_iconTop GitHub Comments

3reactions
nils-christiancommented, Jun 14, 2019

Hi,

We tested some possible solutions in the meantime. The suggested idea of @smcvb works well (custom JPA repository with suitable identifier converter), but this would mean a custom repository for each state stored aggregate. Therefore we decided to use another approach. We used a more generic identifier converter, which would use the Spring conversion mechanism, and added it to the registerAggregateBeanDefinitions in the 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." );
						}
					}
				} )

So the basic idea is to find the type of the aggregate identifier and - unless it is already a String - convert it via the Spring conversion service. One would then have a suitable converter for each identifier.

@Named
final class MyIdConverter implements Converter<String, MyId> {

	...

	@Override
	public MyId convert( final String source ) {
		return MyId.fromString( source );
	}

}

In case of UUID identifiers one would only need the Spring conversion service, as it is already equipped with a String to UUID converter.

The only disadvantage of this approach is that we have to override the whole class to modify the private method. Maybe it would be possible to improve the code in this regard?

Best regards

Nils

2reactions
smcvbcommented, Feb 11, 2020

Reopening seems reasonable to me, so let’s do that 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

State Stored Aggregates - Axon Reference Guide
An Aggregate Root must declare a field that contains the Aggregate Identifier. This identifier must be initialized at the latest when the first...
Read more >
cqrs - Understanding of @AggregateIdentifier ...
1 Answer 1 · for event sourced aggregates all state changing events are stored in a event store · for state stored aggregates...
Read more >
State-Stored Aggregate - Ecotone
An Aggregate is a regular object, which contains state and methods to alter that state. It can be described as Entity, which carry...
Read more >
GenericJPARepository and EventStore at the same time
protected void setPublicationID(String uuid) ... private String demandeur; ... The aggregate identifier field is given a value identical to the event ...
Read more >
CQRS using Java and Axon - Command module
@Aggregate public class ProductAggregate { @AggregateIdentifier private Long id; private String name; private int quantity; ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found