From 3bc65275b910bc71dc16e6fa100a538bd104dc0b Mon Sep 17 00:00:00 2001 From: wonderfulrosemari Date: Wed, 4 Mar 2026 12:11:41 +0900 Subject: [PATCH] Consider convertible argument types in in JDBC query derivation. Closes #2059 Original pull request: #2249 Signed-off-by: wonderfulrosemari --- .../repository/query/JdbcQueryCreator.java | 20 ++++- .../repository/query/PartTreeJdbcQuery.java | 3 +- .../query/PartTreeJdbcQueryUnitTests.java | 77 ++++++++++++++++++- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index 76f90923a..da1286c5f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -24,6 +24,7 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.conversion.AbstractRelationalConverter; import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -47,6 +48,7 @@ import org.springframework.util.Assert; * @author Jens Schauder * @author Myeonghyeon Lee * @author Diego Krupitza + * @author wonderfulrosemari * @since 2.0 */ public class JdbcQueryCreator extends RelationalQueryCreator { @@ -152,7 +154,8 @@ public class JdbcQueryCreator extends RelationalQueryCreator * @param tree the tree structure defining the predicate of the query. * @param parameters parameters for the predicate. */ - static void validate(PartTree tree, Parameters parameters, RelationalMappingContext context) { + static void validate(PartTree tree, Parameters parameters, RelationalMappingContext context, + JdbcConverter converter) { RelationalQueryCreator.validate(tree, parameters); @@ -163,12 +166,12 @@ public class JdbcQueryCreator extends RelationalQueryCreator .getPersistentPropertyPath(part.getProperty()); AggregatePath path = context.getAggregatePath(propertyPath); - path.forEach(JdbcQueryCreator::validateProperty); + path.forEach(pathElement -> validateProperty(pathElement, converter)); } } } - private static void validateProperty(AggregatePath path) { + private static void validateProperty(AggregatePath path, JdbcConverter converter) { if (path.isRoot()) { return; @@ -183,11 +186,20 @@ public class JdbcQueryCreator extends RelationalQueryCreator String.format("Cannot query by multi-valued property: %s", path.getRequiredLeafProperty().getName())); } - if (!path.isEmbedded() && path.isEntity()) { + if (!path.isEmbedded() && path.isEntity() && !hasCustomWriteTarget(path, converter)) { throw new IllegalArgumentException(String.format("Cannot query by nested entity: %s", path.toDotPath())); } } + private static boolean hasCustomWriteTarget(AggregatePath path, JdbcConverter converter) { + + if (!(converter instanceof AbstractRelationalConverter relationalConverter)) { + return false; + } + + return relationalConverter.getConversions().hasCustomWriteTarget(path.getRequiredLeafProperty().getActualType()); + } + /** * Creates {@link ParametrizedQuery} applying the given {@link Criteria} and {@link Sort} definition. * diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java index 8b30524c9..134ecb6e0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java @@ -61,6 +61,7 @@ import org.springframework.util.Assert; * @author Mikhail Polivakha * @author Yunyoung LEE * @author Nikita Konev + * @author wonderfulrosemari * @since 2.0 */ public class PartTreeJdbcQuery extends AbstractJdbcQuery { @@ -145,7 +146,7 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { this.converter = converter; this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType()); - JdbcQueryCreator.validate(this.tree, this.parameters, this.converter.getMappingContext()); + JdbcQueryCreator.validate(this.tree, this.parameters, this.converter.getMappingContext(), this.converter); this.cachedRowMapperFactory = new CachedRowMapperFactory(tree, rowMapperFactory, converter, queryMethod.getResultProcessor()); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 1b60cac18..8f6ca828b 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -33,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; +import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -73,6 +74,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; * @author Jens Schauder * @author Myeonghyeon Lee * @author Diego Krupitza + * @author wonderfulrosemari */ @ExtendWith(MockitoExtension.class) public class PartTreeJdbcQueryUnitTests { @@ -753,15 +755,46 @@ public class PartTreeJdbcQueryUnitTests { .isEqualTo("SELECT COUNT(*) FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name"); } + @Test // GH-2059 + void createsQueryBySimpleDomainPrimitiveWithCustomConverters() throws Exception { + + JdbcCustomConversions conversions = JdbcCustomConversions.create(JdbcPostgresDialect.INSTANCE, it -> { + it.registerConverter(CustomerRefToStringConverter.INSTANCE); + it.registerConverter(StringToCustomerRefConverter.INSTANCE); + }); + JdbcMappingContext localContext = new JdbcMappingContext(); + JdbcConverter localConverter = new MappingJdbcConverter(localContext, mock(RelationResolver.class), conversions, + JdbcTypeFactory.unsupported()); + + JdbcQueryMethod queryMethod = getQueryMethod(CustomerRepository.class, localContext, "findAllByRef", + CustomerRef.class); + PartTreeJdbcQuery jdbcQuery = new PartTreeJdbcQuery(localContext, queryMethod, JdbcH2Dialect.INSTANCE, + localConverter, mock(NamedParameterJdbcOperations.class), mock(RowMapper.class)); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { new CustomerRef("abc") }), + returnedType); + + QueryAssert.assertThat(query).contains(" WHERE \"customers\".\"REF\" = :ref").hasBindValue("ref", "abc"); + } + private PartTreeJdbcQuery createQuery(JdbcQueryMethod queryMethod) { return new PartTreeJdbcQuery(mappingContext, queryMethod, JdbcH2Dialect.INSTANCE, converter, mock(NamedParameterJdbcOperations.class), mock(RowMapper.class)); } private JdbcQueryMethod getQueryMethod(String methodName, Class... parameterTypes) throws Exception { - Method method = UserRepository.class.getMethod(methodName, parameterTypes); - return new JdbcQueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class), - new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), mappingContext); + return getQueryMethod(UserRepository.class, methodName, parameterTypes); + } + + private JdbcQueryMethod getQueryMethod(Class repositoryType, String methodName, Class... parameterTypes) + throws Exception { + return getQueryMethod(repositoryType, mappingContext, methodName, parameterTypes); + } + + private JdbcQueryMethod getQueryMethod(Class repositoryType, JdbcMappingContext mappingContext, String methodName, + Class... parameterTypes) throws Exception { + Method method = repositoryType.getMethod(methodName, parameterTypes); + return new JdbcQueryMethod(method, new DefaultRepositoryMetadata(repositoryType), new SpelAwareProxyProjectionFactory(), + new PropertiesBasedNamedQueries(new Properties()), mappingContext); } private RelationalParametersParameterAccessor getAccessor(JdbcQueryMethod queryMethod, Object[] values) { @@ -866,6 +899,12 @@ public class PartTreeJdbcQueryUnitTests { long countByFirstName(String name); } + @NoRepositoryBean + interface CustomerRepository extends Repository { + + List findAllByRef(CustomerRef ref); + } + @Table("users") static class User { @@ -890,6 +929,16 @@ public class PartTreeJdbcQueryUnitTests { record UserId(Long id, String subId) { } + @Table("customers") + static class Customer { + + @Id Long id; + CustomerRef ref; + } + + record CustomerRef(String value) { + } + record Address(String street, String city) { } @@ -914,4 +963,26 @@ public class PartTreeJdbcQueryUnitTests { return JdbcValue.of(source.ordinal(), JdbcPostgresDialect.INSTANCE.createSqlType("foo", 4711)); } } + + @WritingConverter + enum CustomerRefToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(CustomerRef source) { + return source.value(); + } + } + + @ReadingConverter + enum StringToCustomerRefConverter implements Converter { + + INSTANCE; + + @Override + public CustomerRef convert(String source) { + return new CustomerRef(source); + } + } }