Skip to content

Conversion of jMolecules Identifier implementations to primitive fails with Spring Data JDBC #3118

@ddnyer

Description

@ddnyer

Issue

When using the JMolecules Spring integration with Spring Data JDBC, persisting an entity with an id implementing Identifier fails randomly (sometimes is works, sometimes not). More exactly, it's the conversion of the Identifier to a primitive to populate the SQL query that fails.

Spring Boot version 3.3.1
JMolecules integration version 0.20.0

Stacktrace

The exception is raised when IdentifierToPrimitivesConverter is asked to convert into a UUID an Identifier containing a String id.

Failed to execute InsertRoot{entity=org.example.demo.domain.user.auth.permission.model.Permission@5a2ca12b, idValueSource=PROVIDED}
org.springframework.data.relational.core.conversion.DbActionExecutionException: Failed to execute InsertRoot{entity=org.example.demo.domain.user.auth.permission.model.Permission@5a2ca12b, idValueSource=PROVIDED}
	at org.springframework.data.jdbc.core.AggregateChangeExecutor.execute(AggregateChangeExecutor.java:118)
	at org.springframework.data.jdbc.core.AggregateChangeExecutor.lambda$executeSave$0(AggregateChangeExecutor.java:61)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at org.springframework.data.relational.core.conversion.SaveBatchingAggregateChange.forEachAction(SaveBatchingAggregateChange.java:74)
	at org.springframework.data.jdbc.core.AggregateChangeExecutor.executeSave(AggregateChangeExecutor.java:61)
	at org.springframework.data.jdbc.core.JdbcAggregateTemplate.performSave(JdbcAggregateTemplate.java:491)
	at org.springframework.data.jdbc.core.JdbcAggregateTemplate.save(JdbcAggregateTemplate.java:168)
	at org.springframework.data.jdbc.repository.support.SimpleJdbcRepository.save(SimpleJdbcRepository.java:68)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
	at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516)
	at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
	at jdk.proxy3/jdk.proxy3.$Proxy111.save(Unknown Source)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
	at jdk.proxy3/jdk.proxy3.$Proxy111.save(Unknown Source)
	at org.example.demo.domain.user.auth.permission.PermissionRepositoryTest.createPermission(PermissionRepositoryTest.java:34)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.UUID] for value [AZB3L84udY2tQrXFlU2Gvw]
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:182)
	at org.jmolecules.spring.IdentifierToPrimitivesConverter.convert(IdentifierToPrimitivesConverter.java:116)
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:182)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:165)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.writeValue(MappingRelationalConverter.java:700)
	at org.springframework.data.jdbc.core.convert.MappingJdbcConverter.writeValue(MappingJdbcConverter.java:215)
	at org.springframework.data.jdbc.core.convert.MappingJdbcConverter.writeJdbcValue(MappingJdbcConverter.java:252)
	at org.springframework.data.jdbc.core.convert.JdbcConverter.writeJdbcValue(JdbcConverter.java:53)
	at org.springframework.data.jdbc.core.convert.SqlParametersFactory.addConvertedValue(SqlParametersFactory.java:199)
	at org.springframework.data.jdbc.core.convert.SqlParametersFactory.addConvertedPropertyValue(SqlParametersFactory.java:186)
	at org.springframework.data.jdbc.core.convert.SqlParametersFactory.forInsert(SqlParametersFactory.java:92)
	at org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy.insert(DefaultDataAccessStrategy.java:105)
	at org.springframework.data.jdbc.core.JdbcAggregateChangeExecutionContext.executeInsertRoot(JdbcAggregateChangeExecutionContext.java:83)
	at org.springframework.data.jdbc.core.AggregateChangeExecutor.execute(AggregateChangeExecutor.java:85)
	... 43 more
Caused by: java.lang.IllegalArgumentException: Invalid UUID string: AZB3L84udY2tQrXFlU2Gvw
	at java.base/java.util.UUID.fromString1(UUID.java:282)
	at java.base/java.util.UUID.fromString(UUID.java:260)
	at org.springframework.core.convert.support.StringToUUIDConverter.convert(StringToUUIDConverter.java:37)
	at org.springframework.core.convert.support.StringToUUIDConverter.convert(StringToUUIDConverter.java:32)
	at org.springframework.core.convert.support.GenericConversionService$ConverterAdapter.convert(GenericConversionService.java:358)
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
	... 58 more


Failed to convert from type [java.lang.String] to type [java.util.UUID] for value [AZB3L84udY2tQrXFlU2Gvw]
org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.UUID] for value [AZB3L84udY2tQrXFlU2Gvw]
	at app//org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
	at app//org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:182)
	at app//org.jmolecules.spring.IdentifierToPrimitivesConverter.convert(IdentifierToPrimitivesConverter.java:116)
	at app//org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
	at app//org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:182)
	at app//org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:165)
	at app//org.springframework.data.relational.core.conversion.MappingRelationalConverter.writeValue(MappingRelationalConverter.java:700)
	at app//org.springframework.data.jdbc.core.convert.MappingJdbcConverter.writeValue(MappingJdbcConverter.java:215)
	at app//org.springframework.data.jdbc.core.convert.MappingJdbcConverter.writeJdbcValue(MappingJdbcConverter.java:252)
	at app//org.springframework.data.jdbc.core.convert.JdbcConverter.writeJdbcValue(JdbcConverter.java:53)
	at app//org.springframework.data.jdbc.core.convert.SqlParametersFactory.addConvertedValue(SqlParametersFactory.java:199)
	at app//org.springframework.data.jdbc.core.convert.SqlParametersFactory.addConvertedPropertyValue(SqlParametersFactory.java:186)
	at app//org.springframework.data.jdbc.core.convert.SqlParametersFactory.forInsert(SqlParametersFactory.java:92)
	at app//org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy.insert(DefaultDataAccessStrategy.java:105)
	at app//org.springframework.data.jdbc.core.JdbcAggregateChangeExecutionContext.executeInsertRoot(JdbcAggregateChangeExecutionContext.java:83)
	at app//org.springframework.data.jdbc.core.AggregateChangeExecutor.execute(AggregateChangeExecutor.java:85)
	at app//org.springframework.data.jdbc.core.AggregateChangeExecutor.lambda$executeSave$0(AggregateChangeExecutor.java:61)
	at java.base@21.0.3/java.util.ArrayList.forEach(ArrayList.java:1596)
	at app//org.springframework.data.relational.core.conversion.SaveBatchingAggregateChange.forEachAction(SaveBatchingAggregateChange.java:74)
	at app//org.springframework.data.jdbc.core.AggregateChangeExecutor.executeSave(AggregateChangeExecutor.java:61)
	at app//org.springframework.data.jdbc.core.JdbcAggregateTemplate.performSave(JdbcAggregateTemplate.java:491)
	at app//org.springframework.data.jdbc.core.JdbcAggregateTemplate.save(JdbcAggregateTemplate.java:168)
	at app//org.springframework.data.jdbc.repository.support.SimpleJdbcRepository.save(SimpleJdbcRepository.java:68)
	at java.base@21.0.3/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base@21.0.3/java.lang.reflect.Method.invoke(Method.java:580)
	at app//org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
	at app//org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
	at app//org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
	at app//org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
	at app//org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516)
	at app//org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
	at app//org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628)
	at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at app//org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168)
	at app//org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
	at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at app//org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at app//org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392)
	at app//org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at app//org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
	at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at app//org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at app//org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
	at app/jdk.proxy3/jdk.proxy3.$Proxy111.save(Unknown Source)
	at java.base@21.0.3/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base@21.0.3/java.lang.reflect.Method.invoke(Method.java:580)
	at app//org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
	at app//org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
	at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at app//org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
	at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at app//org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
	at app/jdk.proxy3/jdk.proxy3.$Proxy111.save(Unknown Source)
	at app//org.example.demo.domain.user.auth.permission.PermissionRepositoryTest.createPermission(PermissionRepositoryTest.java:34)
	at java.base@21.0.3/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base@21.0.3/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base@21.0.3/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.IllegalArgumentException: Invalid UUID string: AZB3L84udY2tQrXFlU2Gvw
	at java.base/java.util.UUID.fromString1(UUID.java:282)
	at java.base/java.util.UUID.fromString(UUID.java:260)
	at org.springframework.core.convert.support.StringToUUIDConverter.convert(StringToUUIDConverter.java:37)
	at org.springframework.core.convert.support.StringToUUIDConverter.convert(StringToUUIDConverter.java:32)
	at org.springframework.core.convert.support.GenericConversionService$ConverterAdapter.convert(GenericConversionService.java:358)
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
	... 58 more

Root cause

The issue is in CustomConversions.

In its constructor, CustomConversions creates a list of converters. From DEFAULT_CONVERTERS, it finds IdentifierToPrimitivesConverter from the JMolecule package which registers two converters, one for Identifier -> UUID and one for Identifier -> String. Both pairs are listed as converters and also saved in writingPairs. The order in which they appear in this list is random because IdentifierToPrimitivesConverter use an HashSet to store the supported primitive types used to build the pairs returned by getConvertibleTypes().

Now when getCustomWriteTarget is called to determine the target type of a class implementing Identifier, we get into trouble. It calls getCustomTarget which will pick the first converter with Identifier as source type and return the associated target type. In this case it will randomly return String or UUID. Obviously when the wrong target type is selected, the conversion of the Identifier later on fails.

Sample project

https://github.com/ddnyer/spring-data-jdbc-jmolecules/tree/main

run docker compose up to have a running db then you can run the unit test, it will fail as the wrong target type is returned for one of the Identifier.

Metadata

Metadata

Assignees

Labels

in: coreIssues in core supportstatus: invalidAn issue that we don't feel is valid

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions