diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java index ddfb1e7431..b426bf1b7f 100755 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java @@ -21,28 +21,47 @@ import org.springframework.core.MethodParameter; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.support.JdbcUtil; +import org.springframework.data.relational.core.dialect.DefaultSqlTypeResolver; +import org.springframework.data.relational.core.dialect.SqlTypeResolver; import org.springframework.data.relational.repository.query.RelationalParameters; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; /** * Custom extension of {@link RelationalParameters}. * * @author Mark Paluch + * @author Mikhail Polivakha * @since 3.2.6 */ public class JdbcParameters extends RelationalParameters { /** - * Creates a new {@link JdbcParameters} instance from the given {@link ParametersSource}. + * Creates a new {@link JdbcParameters} instance from the given {@link ParametersSource}. Uses the {@link DefaultSqlTypeResolver}. * * @param parametersSource must not be {@literal null}. */ public JdbcParameters(ParametersSource parametersSource) { super(parametersSource, - methodParameter -> new JdbcParameter(methodParameter, parametersSource.getDomainTypeInformation())); + methodParameter -> new JdbcParameter(methodParameter, parametersSource.getDomainTypeInformation(), + Lazy.of(DefaultSqlTypeResolver.INSTANCE))); + } + + /** + * Creates a new {@link JdbcParameters} instance from the given {@link ParametersSource} and given {@link SqlTypeResolver}. + * + * @param parametersSource must not be {@literal null}. + * @param sqlTypeResolver must not be {@literal null}. + */ + public JdbcParameters(ParametersSource parametersSource, Lazy sqlTypeResolver) { + super(parametersSource, + methodParameter -> new JdbcParameter(methodParameter, parametersSource.getDomainTypeInformation(), sqlTypeResolver)); + + Assert.notNull(sqlTypeResolver, "SqlTypeResolver must not be null"); + Assert.notNull(parametersSource, "ParametersSource must not be null"); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -69,7 +88,7 @@ protected JdbcParameters createFrom(List parameters) { */ public static class JdbcParameter extends RelationalParameter { - private final SQLType sqlType; + private final Lazy sqlType; private final Lazy actualSqlType; /** @@ -77,19 +96,34 @@ public static class JdbcParameter extends RelationalParameter { * * @param parameter must not be {@literal null}. */ - JdbcParameter(MethodParameter parameter, TypeInformation domainType) { + JdbcParameter(MethodParameter parameter, TypeInformation domainType, Lazy sqlTypeResolver) { super(parameter, domainType); TypeInformation typeInformation = getTypeInformation(); - sqlType = JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getType())); + sqlType = Lazy.of(() -> { + SQLType resolvedSqlType = sqlTypeResolver.get().resolveSqlType(this); + + if (resolvedSqlType == null) { + return JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getType())); + } else { + return resolvedSqlType; + } + }); + + actualSqlType = Lazy.of(() -> { + SQLType resolvedActualSqlType = sqlTypeResolver.get().resolveActualSqlType(this); - actualSqlType = Lazy.of(() -> JdbcUtil - .targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getActualType().getType()))); + if (resolvedActualSqlType == null) { + return JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getActualType().getType())); + } else { + return resolvedActualSqlType; + } + }); } public SQLType getSqlType() { - return sqlType; + return sqlType.get(); } public SQLType getActualSqlType() { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java index 6d20a1bcfb..ab5a3e1664 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java @@ -17,6 +17,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -24,6 +25,8 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.relational.core.dialect.DefaultSqlTypeResolver; +import org.springframework.data.relational.core.dialect.SqlTypeResolver; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.repository.Lock; @@ -31,9 +34,11 @@ import org.springframework.data.relational.repository.query.SimpleRelationalEntityMetadata; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.util.Lazy; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.lang.Nullable; @@ -52,6 +57,7 @@ * @author Hebert Coelho * @author Diego Krupitza * @author Mark Paluch + * @author Mikhail Polivakha */ public class JdbcQueryMethod extends QueryMethod { @@ -62,10 +68,19 @@ public class JdbcQueryMethod extends QueryMethod { private @Nullable RelationalEntityMetadata metadata; private final boolean modifyingQuery; + private final SqlTypeResolver sqlTypeResolver; + // TODO: Remove NamedQueries and put it into JdbcQueryLookupStrategy public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries, MappingContext, ? extends RelationalPersistentProperty> mappingContext) { + this(method, metadata, factory, namedQueries, mappingContext, DefaultSqlTypeResolver.INSTANCE); + } + + public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + NamedQueries namedQueries, + MappingContext, ? extends RelationalPersistentProperty> mappingContext, + SqlTypeResolver sqlTypeResolver) { super(method, metadata, factory); this.namedQueries = namedQueries; @@ -73,11 +88,13 @@ public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFac this.mappingContext = mappingContext; this.annotationCache = new ConcurrentReferenceHashMap<>(); this.modifyingQuery = AnnotationUtils.findAnnotation(method, Modifying.class) != null; + this.sqlTypeResolver = sqlTypeResolver; } + // SqlTypeResolver has to be wrapped, becuase the createParameters() is invoked in the parents constructor before child initialization @Override protected Parameters createParameters(ParametersSource parametersSource) { - return new JdbcParameters(parametersSource); + return new JdbcParameters(parametersSource, Lazy.of(() -> this.sqlTypeResolver)); } @Override diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index 7c4ff0d78c..545df2ab34 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jdbc.repository.query; -import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.*; +import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.ResultProcessingConverter; import java.lang.reflect.Array; import java.lang.reflect.Constructor; @@ -32,9 +32,7 @@ import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ValueEvaluationContext; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcValue; @@ -42,11 +40,9 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; -import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionQueryRewriter; @@ -95,8 +91,8 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * and {@link RowMapper}. * - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. + * @param queryMethod must not be {@literal null}. + * @param operations must not be {@literal null}. * @param defaultRowMapper can be {@literal null} (only in case of a modifying query). * @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead. */ @@ -112,10 +108,10 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * and {@link RowMapperFactory}. * - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param rowMapperFactory must not be {@literal null}. - * @param converter must not be {@literal null}. + * @param queryMethod must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param rowMapperFactory must not be {@literal null}. + * @param converter must not be {@literal null}. * @param evaluationContextProvider must not be {@literal null}. * @since 2.3 * @deprecated use alternative constructor @@ -132,11 +128,11 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * and {@link RowMapperFactory}. * - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. + * @param queryMethod must not be {@literal null}. + * @param operations must not be {@literal null}. * @param rowMapperFactory must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param delegate must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param delegate must not be {@literal null}. * @since 3.4 */ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, @@ -148,12 +144,12 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * and {@link RowMapperFactory}. * - * @param query must not be {@literal null} or empty. - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. + * @param query must not be {@literal null} or empty. + * @param queryMethod must not be {@literal null}. + * @param operations must not be {@literal null}. * @param rowMapperFactory must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param delegate must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param delegate must not be {@literal null}. * @since 3.4 */ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, @@ -201,11 +197,11 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * and {@link RowMapperFactory}. * - * @param query must not be {@literal null} or empty. - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param rowMapperFactory must not be {@literal null}. - * @param converter must not be {@literal null}. + * @param query must not be {@literal null} or empty. + * @param queryMethod must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param rowMapperFactory must not be {@literal null}. + * @param converter must not be {@literal null}. * @param evaluationContextProvider must not be {@literal null}. * @since 3.4 * @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead. @@ -215,9 +211,9 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara RowMapperFactory rowMapperFactory, JdbcConverter converter, QueryMethodEvaluationContextProvider evaluationContextProvider) { this(query, queryMethod, operations, rowMapperFactory, converter, new CachingValueExpressionDelegate( - new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), rootObject -> evaluationContextProvider - .getEvaluationContext(queryMethod.getParameters(), new Object[] { rootObject })), - ValueExpressionParser.create())); + new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), + rootObject -> evaluationContextProvider.getEvaluationContext(queryMethod.getParameters(), + new Object[] { rootObject })), ValueExpressionParser.create())); } @Override @@ -413,7 +409,8 @@ private static boolean isUnconfigured(@Nullable Class configuredClass, Class< } @Deprecated(since = "3.4") - public void setBeanFactory(BeanFactory beanFactory) {} + public void setBeanFactory(BeanFactory beanFactory) { + } class CachedRowMapperFactory { @@ -472,19 +469,20 @@ public CachedResultSetExtractorFactory(Supplier> resultSetExtractor String resultSetExtractorRef = getQueryMethod().getResultSetExtractorRef(); Class resultSetExtractorClass = getQueryMethod().getResultSetExtractorClass(); - if (!ObjectUtils.isEmpty(resultSetExtractorRef) - && !isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class)) { + if (!ObjectUtils.isEmpty(resultSetExtractorRef) && !isUnconfigured(resultSetExtractorClass, + ResultSetExtractor.class)) { throw new IllegalArgumentException( "Invalid ResultSetExtractor configuration. Configure either one but not both via @Query(resultSetExtractorRef = …, resultSetExtractorClass = …) for query method " + getQueryMethod()); } - this.configuredResultSetExtractor = !ObjectUtils.isEmpty(resultSetExtractorRef) - || !isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class); + this.configuredResultSetExtractor = + !ObjectUtils.isEmpty(resultSetExtractorRef) || !isUnconfigured(resultSetExtractorClass, + ResultSetExtractor.class); - this.rowMapperConstructor = resultSetExtractorClass != null - ? ClassUtils.getConstructorIfAvailable(resultSetExtractorClass, RowMapper.class) - : null; + this.rowMapperConstructor = resultSetExtractorClass != null ? + ClassUtils.getConstructorIfAvailable(resultSetExtractorClass, RowMapper.class) : + null; this.constructor = resultSetExtractorClass != null ? findPrimaryConstructor(resultSetExtractorClass) : null; this.resultSetExtractorFactory = rowMapper -> { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index fee40edb19..ceb1e13060 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -264,7 +264,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository */ JdbcQueryMethod getJdbcQueryMethod(Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory projectionFactory, NamedQueries namedQueries) { - return new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, getMappingContext()); + return new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, getMappingContext(), getDialect().getSqlTypeResolver()); } /** diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java index ef3b390d52..0364d62e93 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java @@ -19,16 +19,25 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.sql.JDBCType; import java.sql.ResultSet; +import java.sql.SQLType; +import java.sql.Types; +import java.util.List; import java.util.Properties; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.relational.core.dialect.DefaultSqlTypeResolver; +import org.springframework.data.relational.core.dialect.SqlTypeResolver; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.repository.Lock; +import org.springframework.data.relational.repository.query.SqlType; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; @@ -43,6 +52,7 @@ * @author Moises Cisneros * @author Mark Paluch * @author Diego Krupitza + * @author Mikhail Polivakha */ public class JdbcQueryMethodUnitTests { @@ -66,6 +76,8 @@ public void before() { namedQueries = new PropertiesBasedNamedQueries(properties); metadata = mock(RepositoryMetadata.class); + when(metadata.getDomainTypeInformation()).then(invocationOnMock -> TypeInformation.of(Object.class)); + doReturn(String.class).when(metadata).getReturnedDomainClass(any(Method.class)); doReturn(TypeInformation.of(String.class)).when(metadata).getReturnType(any(Method.class)); } @@ -78,6 +90,31 @@ public void returnsSqlStatement() throws NoSuchMethodException { assertThat(queryMethod.getDeclaredQuery()).isEqualTo(QUERY); } + @Test // DATAJDBC-165 + public void testSqlTypeResolver() throws NoSuchMethodException { + + JdbcQueryMethod queryMethod = createJdbcQueryMethod( + "findUserTestMethod", + new DefaultSqlTypeResolver(), + Integer.class, String.class, Integer[].class + ); + + JdbcParameters parameters = queryMethod.getParameters(); + + SQLType first = parameters.getParameter(0).getSqlType(); + SQLType second = parameters.getParameter(1).getSqlType(); + SQLType thirdActual = parameters.getParameter(2).getActualSqlType(); + + Assertions.assertThat(first.getName()).isEqualTo(JDBCType.TINYINT.getName()); + Assertions.assertThat(first.getVendorTypeNumber()).isEqualTo(Types.TINYINT); + + Assertions.assertThat(second.getName()).isEqualTo(JDBCType.VARCHAR.getName()); + Assertions.assertThat(second.getVendorTypeNumber()).isEqualTo(Types.VARCHAR); + + Assertions.assertThat(thirdActual.getName()).isEqualTo(JDBCType.SMALLINT.getName()); + Assertions.assertThat(thirdActual.getVendorTypeNumber()).isEqualTo(Types.SMALLINT); + } + @Test // DATAJDBC-165 public void returnsSpecifiedRowMapperClass() throws NoSuchMethodException { @@ -102,12 +139,6 @@ public void returnsSpecifiedSqlStatementIfNameAndValueAreGiven() throws NoSuchMe } - private JdbcQueryMethod createJdbcQueryMethod(String methodName) throws NoSuchMethodException { - - Method method = JdbcQueryMethodUnitTests.class.getDeclaredMethod(methodName); - return new JdbcQueryMethod(method, metadata, mock(ProjectionFactory.class), namedQueries, mappingContext); - } - @Test // DATAJDBC-234 public void returnsImplicitlyNamedQuery() throws NoSuchMethodException { @@ -148,10 +179,27 @@ void returnsQueryMethodWithCorrectLockTypeNoLock() throws NoSuchMethodException assertThat(queryMethodWithWriteLock.lookupLockAnnotation()).isEmpty(); } + private JdbcQueryMethod createJdbcQueryMethod(String methodName) throws NoSuchMethodException { + return createJdbcQueryMethod(methodName, new DefaultSqlTypeResolver()); + } + + private JdbcQueryMethod createJdbcQueryMethod(String methodName, SqlTypeResolver sqlTypeResolver, Class... args) throws NoSuchMethodException { + + Method method = JdbcQueryMethodUnitTests.class.getDeclaredMethod(methodName, args); + return new JdbcQueryMethod(method, metadata, mock(ProjectionFactory.class), namedQueries, mappingContext, sqlTypeResolver); + } + @Lock(LockMode.PESSIMISTIC_WRITE) @Query private void queryMethodWithWriteLock() {} + @Query + private void findUserTestMethod( + @SqlType(name = "TINYINT", vendorTypeNumber = Types.TINYINT) Integer age, + String name, + @SqlType(name = "SMALLINT", vendorTypeNumber = Types.SMALLINT) Integer[] statuses + ) {} + @Lock(LockMode.PESSIMISTIC_READ) @Query private void queryMethodWithReadLock() {} 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 27f4a47c29..80f37d5f3f 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 @@ -19,7 +19,13 @@ import static org.assertj.core.api.SoftAssertions.*; import static org.mockito.Mockito.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.sql.JDBCType; +import java.sql.SQLType; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -35,14 +41,17 @@ import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.dialect.H2Dialect; +import org.springframework.data.relational.core.dialect.SqlTypeResolver; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.repository.Lock; +import org.springframework.data.relational.repository.query.RelationalParameters; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.Repository; @@ -610,6 +619,18 @@ public void createsQueryWithLimitToFindEntitiesByStringAttribute() throws Except assertThat(query.getQuery()).isEqualTo(expectedSql); } + @Test // DATAJDBC-2020 + public void testCustomSqlTypeResolverApplied() throws Exception { + + JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameAndAgeIn", new TestSqlTypeResolver(), String.class, Collection.class); + + JdbcParameter firstNameParam = queryMethod.getParameters().getParameter(0); + JdbcParameter agesInParam = queryMethod.getParameters().getParameter(1); + + assertThat(firstNameParam.getSqlType()).isEqualTo(JDBCType.CLOB); + assertThat(agesInParam.getActualSqlType()).isEqualTo(JDBCType.TINYINT); + } + @Test // DATAJDBC-318 public void createsQueryToFindFirstEntityByStringAttribute() throws Exception { @@ -679,6 +700,12 @@ private JdbcQueryMethod getQueryMethod(String methodName, Class... parameterT new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), mappingContext); } + private JdbcQueryMethod getQueryMethod(String methodName, SqlTypeResolver sqlTypeResolver, 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, sqlTypeResolver); + } + private RelationalParametersParameterAccessor getAccessor(JdbcQueryMethod queryMethod, Object[] values) { return new RelationalParametersParameterAccessor(queryMethod, values); } @@ -696,6 +723,8 @@ interface UserRepository extends Repository { List findAllByHated(Hobby hobby); + List findAllByFirstNameAndAgeIn(String firstName, Collection ages); + List findAllByHatedName(String name); List findAllByHobbies(Object hobbies); @@ -775,6 +804,35 @@ interface UserRepository extends Repository { long countByFirstName(String name); } + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @interface MySqlType { } + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @interface MyActualSqlType { } + + static class TestSqlTypeResolver implements SqlTypeResolver { + + @Override + public SQLType resolveSqlType(RelationalParameters.RelationalParameter relationalParameter) { + if (relationalParameter.getMethodParameter().hasParameterAnnotation(MySqlType.class)) { + return JDBCType.CLOB; + } else { + return null; + } + } + + @Override + public SQLType resolveActualSqlType(RelationalParameters.RelationalParameter relationalParameter) { + if (relationalParameter.getMethodParameter().hasParameterAnnotation(MyActualSqlType.class)) { + return JDBCType.TINYINT; + } else { + return null; + } + } + } + @Table("users") static class User { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index c0d9cd5bf2..f07f65bef3 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -18,11 +18,17 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.sql.JDBCType; import java.sql.ResultSet; +import java.sql.SQLType; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Properties; @@ -50,9 +56,12 @@ import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.mapping.JdbcValue; +import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.relational.core.dialect.SqlTypeResolver; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.repository.query.RelationalParameters; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; @@ -278,6 +287,23 @@ void limitNotSupported() { .isInstanceOf(UnsupportedOperationException.class); } + @Test // GH-2020 + void testCustomSqlTypeResolution() { + + JdbcQueryMethod queryMethod = createMethod("findByWithSqlTypeResolver", new TestSqlTypResolver(), Integer.class, Collection.class); + + StringBasedJdbcQuery stringBasedJdbcQuery = new StringBasedJdbcQuery(queryMethod, operations, + result -> defaultRowMapper, converter, delegate); + + JdbcParameters parameters = stringBasedJdbcQuery.getQueryMethod().getParameters(); + + JdbcParameter tinyInt = parameters.getParameter(0); + JdbcParameter smallIntActual = parameters.getParameter(1); + + assertThat(tinyInt.getSqlType()).isEqualTo(JDBCType.TINYINT); + assertThat(smallIntActual.getActualSqlType()).isEqualTo(JDBCType.SMALLINT); + } + @Test // GH-1212 void convertsEnumCollectionParameterIntoStringCollectionParameter() { @@ -433,6 +459,13 @@ private JdbcQueryMethod createMethod(String methodName, Class... paramTypes) new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), this.context); } + private JdbcQueryMethod createMethod(String methodName, SqlTypeResolver sqlTypeResolver, Class... paramTypes) { + + Method method = ReflectionUtils.findMethod(MyRepository.class, methodName, paramTypes); + return new JdbcQueryMethod(method, new DefaultRepositoryMetadata(MyRepository.class), + new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), this.context, sqlTypeResolver); + } + private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod) { return createQuery(queryMethod, null, null); } @@ -497,6 +530,9 @@ interface MyRepository extends Repository { @Query(value = "some sql statement") List findByListContainer(ListContainer value); + @Query(value = "SELECT * FROM my_table WHERE t = ? AND f IN (?)") + List findByWithSqlTypeResolver(@MySqlType Integer tinyeInt, @MyActualSqlType Collection smallInt); + @Query("SELECT * FROM table WHERE c = :#{myext.testValue} AND c2 = :#{myext.doSomething()}") Object findBySpelExpression(Object object); @@ -507,6 +543,35 @@ interface MyRepository extends Repository { Object findByListOfTuples(@Param("tuples") List tuples); } + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @interface MySqlType { } + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @interface MyActualSqlType { } + + static class TestSqlTypResolver implements SqlTypeResolver { + + @Override + public SQLType resolveSqlType(RelationalParameters.RelationalParameter relationalParameter) { + if (relationalParameter.getMethodParameter().hasParameterAnnotation(MySqlType.class)) { + return JDBCType.TINYINT; + } else { + return null; + } + } + + @Override + public SQLType resolveActualSqlType(RelationalParameters.RelationalParameter relationalParameter) { + if (relationalParameter.getMethodParameter().hasParameterAnnotation(MyActualSqlType.class)) { + return JDBCType.SMALLINT; + } else { + return null; + } + } + } + private static class CustomRowMapper implements RowMapper { @Override diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/DefaultSqlTypeResolver.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/DefaultSqlTypeResolver.java new file mode 100644 index 0000000000..aae645eb34 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/DefaultSqlTypeResolver.java @@ -0,0 +1,98 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.relational.core.dialect; + +import java.sql.SQLType; + +import org.springframework.data.relational.repository.query.RelationalParameters; +import org.springframework.data.relational.repository.query.SqlType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link SqlTypeResolver}. Capable to resolve the {@link SqlType} annotation + * on the {@link java.lang.annotation.ElementType#PARAMETER method parameters}, like this: + *

+ *

+ * List findByAge(@SqlType(name = "TINYINT", vendorTypeNumber = Types.TINYINT) byte age);
+ * 
+ * + * Qualification of the actual {@link SQLType} (the sql type of the component), then the following needs to be done: + *
+ * List findByAgeIn(@SqlType(name = "TINYINT", vendorTypeNumber = Types.TINYINT) Integer[] age);
+ * 
+ * + * @author Mikhail Polivakha + */ +public class DefaultSqlTypeResolver implements SqlTypeResolver { + + public static DefaultSqlTypeResolver INSTANCE = new DefaultSqlTypeResolver(); + + @Override + @Nullable + public SQLType resolveSqlType(RelationalParameters.RelationalParameter relationalParameter) { + return resolveInternally(relationalParameter); + } + + @Override + @Nullable + public SQLType resolveActualSqlType(RelationalParameters.RelationalParameter relationalParameter) { + return resolveInternally(relationalParameter); + } + + private static AnnotationBasedSqlType resolveInternally( + RelationalParameters.RelationalParameter relationalParameter) { + SqlType parameterAnnotation = relationalParameter.getMethodParameter().getParameterAnnotation(SqlType.class); + + if (parameterAnnotation != null) { + return new AnnotationBasedSqlType(parameterAnnotation); + } else { + return null; + } + } + + /** + * {@link SQLType} determined from the {@link SqlType} annotation. + * + * @author Mikhail Polivakha + */ + protected static class AnnotationBasedSqlType implements SQLType { + + private final SqlType sqlType; + + public AnnotationBasedSqlType(SqlType sqlType) { + Assert.notNull(sqlType, "sqlType must not be null"); + + this.sqlType = sqlType; + } + + @Override + public String getName() { + return sqlType.name(); + } + + @Override + public String getVendor() { + return "Spring Data JDBC"; + } + + @Override + public Integer getVendorTypeNumber() { + return sqlType.vendorTypeNumber(); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java index 9214389d58..e10bc670a3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java @@ -147,4 +147,13 @@ default SimpleFunction getExistsFunction() { default boolean supportsSingleQueryLoading() { return true; } + + /** + * Returns a {@link SqlTypeResolver} of this dialect. + * + * @since 4.0 + */ + default SqlTypeResolver getSqlTypeResolver() { + return DefaultSqlTypeResolver.INSTANCE; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlTypeResolver.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlTypeResolver.java new file mode 100644 index 0000000000..5116ad887f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlTypeResolver.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.relational.core.dialect; + +import java.sql.SQLType; + +import org.springframework.data.relational.repository.query.RelationalParameters.RelationalParameter; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * Common interface for all objects capable to resolve the {@link SQLType} to be used for a give method parameter. + * + * @author Mikhail Polivakha + */ +public interface SqlTypeResolver { + + /** + * Resolving the {@link SQLType} from the given {@link RelationalParameter}. + * + * @param relationalParameter the parameter of the query method + * @return {@code null} in case the given {@link SqlTypeResolver} cannot or do not want to determine the + * {@link SQLType} of the given parameter + */ + @Nullable + SQLType resolveSqlType(RelationalParameter relationalParameter); + + /** + * Resolving the {@link SQLType} from the given {@link RelationalParameter}. The definition of "actual" + * type can be looked up in the {@link TypeInformation#getActualType()}. + * + * @param relationalParameter the parameter of the query method + * @return {@code null} in case the given {@link SqlTypeResolver} cannot or do not want to determine the + * actual {@link SQLType} of the given parameter + */ + @Nullable + SQLType resolveActualSqlType(RelationalParameter relationalParameter); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java index 16a4588a11..c6a0158ba6 100755 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java @@ -66,6 +66,7 @@ protected RelationalParameters createFrom(List parameters) public static class RelationalParameter extends Parameter { private final TypeInformation typeInformation; + private final MethodParameter methodParameter; /** * Creates a new {@link RelationalParameter}. @@ -75,7 +76,7 @@ public static class RelationalParameter extends Parameter { protected RelationalParameter(MethodParameter parameter, TypeInformation domainType) { super(parameter, domainType); this.typeInformation = TypeInformation.fromMethodParameter(parameter); - + this.methodParameter = parameter; } public ResolvableType getResolvableType() { @@ -85,5 +86,9 @@ public ResolvableType getResolvableType() { public TypeInformation getTypeInformation() { return typeInformation; } + + public MethodParameter getMethodParameter() { + return methodParameter; + } } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/SqlType.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/SqlType.java new file mode 100644 index 0000000000..ea4814a1f6 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/SqlType.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.relational.repository.query; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.sql.SQLType; + +import org.springframework.data.relational.core.dialect.DefaultSqlTypeResolver; + +/** + * Serves as a hint to the {@link DefaultSqlTypeResolver}, that signals the {@link java.sql.SQLType} to be used. + * The arguments of this annotation are identical to the methods on {@link java.sql.SQLType} interface, expect for + * the {@link SQLType#getVendor()}, which is absent, because it typically does not matter as such for the underlying + * JDBC drivers. The examples of usage, can be found in javadoc of {@link DefaultSqlTypeResolver}. + * + * @see DefaultSqlTypeResolver + * @author Mikhail Polivakha + */ +@Documented +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SqlType { + + /** + * Returns the {@code SQLType} name that represents a SQL data type. + * + * @return The name of this {@code SQLType}. + */ + String name(); + + /** + * Returns the vendor specific type number for the data type. + * + * @return An Integer representing the vendor specific data type + */ + int vendorTypeNumber(); +}