Axon 4.0 + JPA with UUID identifier causes "id of the wrong type error"

Hi there,

For some background, I’m playing around with Axon Framework 4.0 + Spring Boot + JPA/Hibernate. To the best of my understanding, java.util.UUIDs should be a supported type for an @AggregateIdentifier. However, when using UUID as the identifier, there appears to be an issue fetching the JPA entity associated with the aggregate:

`

2018-10-21 23:42:31.756 INFO 87606 — [mmandReceiver-4] o.a.a.c.command.AxonServerCommandBus : DispatchLocal: failure n.s.o.l.commands.CompleteProjectCommand - Provided id of the wrong type for class n.s.o.l.domain.Project. Expected: class java.util.UUID, got class java.lang.String

java.lang.IllegalArgumentException: Provided id of the wrong type for class n.s.o.l.domain.Project. Expected: class java.util.UUID, got class java.lang.String
at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3466) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3419) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_181]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_181]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_181]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_181]
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:305) ~[spring-orm-5.0.10.RELEASE.jar:5.0.10.RELEASE]
at com.sun.proxy.$Proxy130.find(Unknown Source) ~[na:na]
at org.axonframework.modelling.command.GenericJpaRepository.doLoadWithLock(GenericJpaRepository.java:110) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.modelling.command.GenericJpaRepository.doLoadWithLock(GenericJpaRepository.java:53) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.modelling.command.LockingRepository.doLoad(LockingRepository.java:118) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.modelling.command.LockingRepository.doLoad(LockingRepository.java:52) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.modelling.command.AbstractRepository.lambda$load$4(AbstractRepository.java:116) ~[axon-modelling-4.0.jar:4.0]
at java.util.HashMap.computeIfAbsent(HashMap.java:1127) ~[na:1.8.0_181]
at org.axonframework.modelling.command.AbstractRepository.load(AbstractRepository.java:115) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.modelling.command.AggregateAnnotationCommandHandler$AggregateCommandHandler.handle(AggregateAnnotationCommandHandler.java:364) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.modelling.command.AggregateAnnotationCommandHandler$AggregateCommandHandler.handle(AggregateAnnotationCommandHandler.java:352) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.modelling.command.AggregateAnnotationCommandHandler.handle(AggregateAnnotationCommandHandler.java:140) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.modelling.command.AggregateAnnotationCommandHandler.handle(AggregateAnnotationCommandHandler.java:53) ~[axon-modelling-4.0.jar:4.0]
at org.axonframework.messaging.DefaultInterceptorChain.proceed(DefaultInterceptorChain.java:57) ~[axon-messaging-4.0.jar:4.0]
at org.axonframework.messaging.interceptors.CorrelationDataInterceptor.handle(CorrelationDataInterceptor.java:65) ~[axon-messaging-4.0.jar:4.0]
at org.axonframework.messaging.DefaultInterceptorChain.proceed(DefaultInterceptorChain.java:55) ~[axon-messaging-4.0.jar:4.0]
at org.axonframework.messaging.unitofwork.DefaultUnitOfWork.executeWithResult(DefaultUnitOfWork.java:74) ~[axon-messaging-4.0.jar:4.0]
at org.axonframework.commandhandling.SimpleCommandBus.handle(SimpleCommandBus.java:176) [axon-messaging-4.0.jar:4.0]
at org.axonframework.commandhandling.SimpleCommandBus.doDispatch(SimpleCommandBus.java:141) [axon-messaging-4.0.jar:4.0]
at org.axonframework.commandhandling.SimpleCommandBus.dispatch(SimpleCommandBus.java:110) [axon-messaging-4.0.jar:4.0]
at org.axonframework.axonserver.connector.command.AxonServerCommandBus$CommandRouterSubscriber.dispatchLocal(AxonServerCommandBus.java:365) ~[axon-server-connector-4.0.jar:4.0]
at org.axonframework.axonserver.connector.command.AxonServerCommandBus$CommandRouterSubscriber.processCommand(AxonServerCommandBus.java:276) ~[axon-server-connector-4.0.jar:4.0]
at org.axonframework.axonserver.connector.command.AxonServerCommandBus$CommandRouterSubscriber.commandExecutor(AxonServerCommandBus.java:223) ~[axon-server-connector-4.0.jar:4.0]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_181]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_181]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_181]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_181]
at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_181]
Caused by: org.hibernate.TypeMismatchException: Provided id of the wrong type for class net.spantree.os.ledger.domain.Project. Expected: class java.util.UUID, got class java.lang.String
at org.hibernate.event.internal.DefaultLoadEventListener.checkIdClass(DefaultLoadEventListener.java:166) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:86) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1240) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
at org.hibernate.internal.SessionImpl.access$1900(SessionImpl.java:204) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
at org.hibernate.internal.SessionImpl$IdentifierLoadAccessImpl.doLoad(SessionImpl.java:2835) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
at org.hibernate.internal.SessionImpl$IdentifierLoadAccessImpl.load(SessionImpl.java:2816) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3445) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
… 33 common frames omitted

`

Steps to reproduce

  1. Checkout this project: https://gitlab.spantree.net/divideby0/axoniq-uuid-example
  2. Run docker-compose up -d
  3. Run ./gradlew test --tests ProjectIntegrationTest
    For convenience, this is the part of the test that is failing with the error:

https://gitlab.spantree.net/divideby0/axoniq-uuid-example/blob/master/src/test/kotlin/net/spantree/os/ledger/tests/integration/ProjectIntegrationTest.kt#L33

From what I can tell, Axon’s DefaultJpaRepository looks up all aggregate entities via the find method of the JPA EntityManager:

https://github.com/AxonFramework/AxonFramework/blob/31fe8a7eae7bf019b7e7e4fc10d5ee2230e0b56b/modelling/src/main/java/org/axonframework/modelling/command/GenericJpaRepository.java#L111

However, it doesn’t seem to introspect the aggregate type to determine if it needs to convert the command’s VersionedAggregateIdentifier to its native value:

https://github.com/AxonFramework/AxonFramework/blob/31fe8a7eae7bf019b7e7e4fc10d5ee2230e0b56b/modelling/src/main/java/org/axonframework/modelling/command/AggregateAnnotationCommandHandler.java#L363

Even if the @AggregateIdentifier is some other type, the VersionedAggregateIdentifier will always be a String:

https://github.com/AxonFramework/AxonFramework/blob/31fe8a7eae7bf019b7e7e4fc10d5ee2230e0b56b/modelling/src/main/java/org/axonframework/modelling/command/VersionedAggregateIdentifier.java#L49

I noticed there was a identifierConverter function passed into the builder of the DefaultJpaRepository, but it appears to be hardcoded to the identity function which just returns the incoming argument:

https://github.com/AxonFramework/AxonFramework/blob/31fe8a7eae7bf019b7e7e4fc10d5ee2230e0b56b/modelling/src/main/java/org/axonframework/modelling/command/GenericJpaRepository.java#L193

What’s the best way to handle this in Axon 4.0? Are there ways of overriding the identifierConverter for specific aggregate repositories?

Hi Cedric,

internally, Axon uses String identifiers to route commands to their destination. The Repository interface also declares a String as type for aggregate identifier.
However, that does not mean you cannot use something else as identifier, yourself. You will, however, need to tell Axon how to convert the String to your own type.

You can do so by specifying an “identifierConverter” in the configuration of your GenericJpaRepository. Basically, I guess you would want to provide UUID::fromString as parameter.

Cheers,

Allard

Hi Cedric, Allard,

I stand by Allard his response by the way, that’s not why I am replying over it.
I just took the liberty to check out the project repository you’ve shared with us Cedric, and I am guessing you might be mixing a couple of things with your Aggregate.

I see you are using the @EventSourcingHandler annotation, but also the @Entity annotation on class level.

The first suggests you want to do event sourcing, but the latter will ensure the Aggregate will not be event sourced because it is marked as an entity.

So, I was wondering whether that was by design or not.

Assuming you want to make it an Event Sourcing Aggregate, my hunch is you’ll lose the shared exception as well. The @AggregateIdentifier annotatied field in that scenario will no longer have to coop with Hibernate to begin with, if it isn’t an entity directly.

That’s my 2 cents to this scenario.

Cheers,
Steven

Thank you both for the context, that helps clarify things. In my case, I wanted to leverage entity behavior to perform an abstract search on all the fields at query-time. For example, I may want to search for all Projects with a certain status and a name containing a specific term. At the same time, I also wanted to a replayable event log of all mutations to each entity. I haven’t mastered the Axon Query mechanism yet, but to the best of my understanding, it relies on projecting view models for all possible access patterns. If that’s the case, I worry it could get noisy when projecting out the cross-product of searchable fields at their various degrees of cardinality.

I did find it odd to use both @Entity and @AggregateIdentifier annotations, so perhaps that’s something I can revisit. I’m also newer to Axon Framework so please advise if there’s a more idiomatic approach for handling these concerns.

Allard, also thanks for validating my hypothesis that overriding the identifyConverter could fix the UUID issue. As I was reading through the code, I was struggling to figure out a clean way to replace just that one function without copying over all the other boilerplate in the builder. Is it best to expose a bean constructed from a builder defined here:

https://github.com/AxonFramework/AxonFramework/blob/31fe8a7eae7bf019b7e7e4fc10d5ee2230e0b56b/modelling/src/main/java/org/axonframework/modelling/command/GenericJpaRepository.java#L81

I assume I could then just override the identifyConverter before calling the GenericJpaRepository constructor with that builder as an argument. Alternatively, is there a way just to expose and wire a bean for the identityConverter itself and have Spring wire it in automatically?

Also, if there are code samples for the best way to do this, please feel free to pass them along. I wasn’t able to dig anything up in the tests, but perhaps I was looking in the wrong place.

Hi,

unfortunately, if you don’t use the default GenericJpaRepository instance, you’ll need to build your own. We currently don’t allow for partial configurations to be merged, in some way. Interesting idea, though…

Regarding “I did find it odd to use both @Entity and @AggregateIdentifier annotations”, you can actually also just use JPA’s @Id to annotate the aggregate identifier field. The only annotation that you’d really need is the @Aggregate on the class level, to have Axon autoconfigure components for an aggregate.

Cheers,

Allard