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
But there are two major problems:
.identifierConverter(UUID::fromString) the command can be handled.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.
Hi @OLibutzki, I think I can alleviate the two major problems you've noted here:
@Aggregate annotation I'd assume. The @Aggregate annotation has a field repository of type String. If you fill in the name of the repository you'd want to use for that Aggregate instance, it'll be wired upon start up. Thus, you should be able to create your GenericJpaRepository through it's Builder and provide the identifierConverter you wish.fromString function as well. When I am leveraging typed ID's for Aggregate/Entities, more often that class contains a UUID any how (which as we now, has a fromString function.Concluding, I think point 1 is luckily covered by the framework.
I trust in you that this wasn't clear from the Reference Guide; I can assure you we're consistently looking to improve the guide. It sounds like the Spring configuration is one of those areas which still needs some improvement.
Point 2 is something I to be honest do not view as to big of a struggle. If my assumptions on your question were incorrect in this area, please tell me so.
If you're however okay with the above given answer, I think we can close this issue of for now. :-)
Thanks for your answer @smcvb! I will try your suggestion and will come back to this issue after my evaluation. I suggest we keep this issue opened until I can provide some feedback.
Of course we'll keep it open @OLibutzki! Let us know as soon as you've had to chance to look into this.
By the way, I've checked whether the Reference Guide stated the setting of the repository when using Spring. You can find the bit I've just described here.
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
We have discussed this within our team and decided, for now, not to change any of the APIs for this purpose. Once we start working on an API-breaking release (i.e. Axon 5+), we will reconsider the Repository API to support non-String identifiers as well.
For the time being, we consider the explicit configuration of identifierConverters an acceptable solution.
Hi @abuijze and @smcvb,
as we talked about this issue in your office and you saw a chance to get this working in a backward comaptible manner by using default methods: Shall we open this issue again oder do you prefer to have a new one?
Cheers
Oliver
Reopening seems reasonable to me, so let's do that :-)
Any update on this? Will it be part of Axon 5 or is there any other solution implemented in the meanwhile?
Thanks
Jurgen
First, thanks for showing interest in the feature @jurgenbosch!
To come to your question, we typically update issues accordingly when work is progressing or has happened on them.
As it stands, no effort has been taken here since February 11th (as seen by my previous comment).
The ideas are present to "resolve" this, but it is a matter of priority at this stage.
Given the amount of work and other issues added to milestone 4.5 (the upcoming release), we have decided not to include this just yet. Know of course that there is always room for contribution when it comes to _any_ of the issues up here.
If you have the time, and interest, to provide a solution, that would obviously mean a more speedy introduction of this feature into the framework.
Most helpful comment
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.
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.
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