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

323 views
Skip to first unread message

Cedric Hurst

unread,
Oct 22, 2018, 1:09:54 AM10/22/18
to Axon Framework Users
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:


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


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:


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


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:


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

Allard Buijze

unread,
Oct 23, 2018, 2:56:38 AM10/23/18
to axonfr...@googlegroups.com
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

--
You received this message because you are subscribed to the Google Groups "Axon Framework Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to axonframewor...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
--
Allard Buijze
CTO

E: allard...@axoniq.io
T: +31 6 34 73 99 89

Steven van Beelen

unread,
Oct 23, 2018, 5:42:47 AM10/23/18
to axonfr...@googlegroups.com
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

Cedric Hurst

unread,
Oct 25, 2018, 12:40:51 AM10/25/18
to Axon Framework Users
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:


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.

Allard Buijze

unread,
Oct 26, 2018, 7:27:07 AM10/26/18
to axonfr...@googlegroups.com
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
Reply all
Reply to author
Forward
0 new messages