Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d92a729

Browse files
committedApr 5, 2025·
GH-2020 Added SqlTypeResolver abstraction
Signed-off-by: mipo256 <mikhailpolivakha@gmail.com>
1 parent f3dc789 commit d92a729

File tree

12 files changed

+488
-76
lines changed

12 files changed

+488
-76
lines changed
 

‎spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,47 @@
2121
import org.springframework.core.MethodParameter;
2222
import org.springframework.data.jdbc.core.convert.JdbcColumnTypes;
2323
import org.springframework.data.jdbc.support.JdbcUtil;
24+
import org.springframework.data.relational.core.dialect.DefaultSqlTypeResolver;
25+
import org.springframework.data.relational.core.dialect.SqlTypeResolver;
2426
import org.springframework.data.relational.repository.query.RelationalParameters;
2527
import org.springframework.data.repository.query.Parameter;
2628
import org.springframework.data.repository.query.ParametersSource;
2729
import org.springframework.data.util.Lazy;
2830
import org.springframework.data.util.TypeInformation;
31+
import org.springframework.util.Assert;
2932

3033
/**
3134
* Custom extension of {@link RelationalParameters}.
3235
*
3336
* @author Mark Paluch
37+
* @author Mikhail Polivakha
3438
* @since 3.2.6
3539
*/
3640
public class JdbcParameters extends RelationalParameters {
3741

3842
/**
39-
* Creates a new {@link JdbcParameters} instance from the given {@link ParametersSource}.
43+
* Creates a new {@link JdbcParameters} instance from the given {@link ParametersSource}. Uses the {@link DefaultSqlTypeResolver}.
4044
*
4145
* @param parametersSource must not be {@literal null}.
4246
*/
4347
public JdbcParameters(ParametersSource parametersSource) {
4448
super(parametersSource,
45-
methodParameter -> new JdbcParameter(methodParameter, parametersSource.getDomainTypeInformation()));
49+
methodParameter -> new JdbcParameter(methodParameter, parametersSource.getDomainTypeInformation(),
50+
Lazy.of(DefaultSqlTypeResolver.INSTANCE)));
51+
}
52+
53+
/**
54+
* Creates a new {@link JdbcParameters} instance from the given {@link ParametersSource} and given {@link SqlTypeResolver}.
55+
*
56+
* @param parametersSource must not be {@literal null}.
57+
* @param sqlTypeResolver must not be {@literal null}.
58+
*/
59+
public JdbcParameters(ParametersSource parametersSource, Lazy<SqlTypeResolver> sqlTypeResolver) {
60+
super(parametersSource,
61+
methodParameter -> new JdbcParameter(methodParameter, parametersSource.getDomainTypeInformation(), sqlTypeResolver));
62+
63+
Assert.notNull(sqlTypeResolver, "SqlTypeResolver must not be null");
64+
Assert.notNull(parametersSource, "ParametersSource must not be null");
4665
}
4766

4867
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -69,27 +88,42 @@ protected JdbcParameters createFrom(List<RelationalParameter> parameters) {
6988
*/
7089
public static class JdbcParameter extends RelationalParameter {
7190

72-
private final SQLType sqlType;
91+
private final Lazy<SQLType> sqlType;
7392
private final Lazy<SQLType> actualSqlType;
7493

7594
/**
7695
* Creates a new {@link RelationalParameter}.
7796
*
7897
* @param parameter must not be {@literal null}.
7998
*/
80-
JdbcParameter(MethodParameter parameter, TypeInformation<?> domainType) {
99+
JdbcParameter(MethodParameter parameter, TypeInformation<?> domainType, Lazy<SqlTypeResolver> sqlTypeResolver) {
81100
super(parameter, domainType);
82101

83102
TypeInformation<?> typeInformation = getTypeInformation();
84103

85-
sqlType = JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getType()));
104+
sqlType = Lazy.of(() -> {
105+
SQLType resolvedSqlType = sqlTypeResolver.get().resolveSqlType(this);
106+
107+
if (resolvedSqlType == null) {
108+
return JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getType()));
109+
} else {
110+
return resolvedSqlType;
111+
}
112+
});
113+
114+
actualSqlType = Lazy.of(() -> {
115+
SQLType resolvedActualSqlType = sqlTypeResolver.get().resolveActualSqlType(this);
86116

87-
actualSqlType = Lazy.of(() -> JdbcUtil
88-
.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getActualType().getType())));
117+
if (resolvedActualSqlType == null) {
118+
return JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getActualType().getType()));
119+
} else {
120+
return resolvedActualSqlType;
121+
}
122+
});
89123
}
90124

91125
public SQLType getSqlType() {
92-
return sqlType;
126+
return sqlType.get();
93127
}
94128

95129
public SQLType getActualSqlType() {

‎spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,28 @@
1717

1818
import java.lang.annotation.Annotation;
1919
import java.lang.reflect.Method;
20+
import java.util.List;
2021
import java.util.Map;
2122
import java.util.Optional;
2223

2324
import org.springframework.core.annotation.AnnotatedElementUtils;
2425
import org.springframework.core.annotation.AnnotationUtils;
2526
import org.springframework.data.mapping.context.MappingContext;
2627
import org.springframework.data.projection.ProjectionFactory;
28+
import org.springframework.data.relational.core.dialect.DefaultSqlTypeResolver;
29+
import org.springframework.data.relational.core.dialect.SqlTypeResolver;
2730
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
2831
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
2932
import org.springframework.data.relational.repository.Lock;
3033
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
3134
import org.springframework.data.relational.repository.query.SimpleRelationalEntityMetadata;
3235
import org.springframework.data.repository.core.NamedQueries;
3336
import org.springframework.data.repository.core.RepositoryMetadata;
37+
import org.springframework.data.repository.query.Parameter;
3438
import org.springframework.data.repository.query.Parameters;
3539
import org.springframework.data.repository.query.ParametersSource;
3640
import org.springframework.data.repository.query.QueryMethod;
41+
import org.springframework.data.util.Lazy;
3742
import org.springframework.jdbc.core.ResultSetExtractor;
3843
import org.springframework.jdbc.core.RowMapper;
3944
import org.springframework.lang.Nullable;
@@ -52,6 +57,7 @@
5257
* @author Hebert Coelho
5358
* @author Diego Krupitza
5459
* @author Mark Paluch
60+
* @author Mikhail Polivakha
5561
*/
5662
public class JdbcQueryMethod extends QueryMethod {
5763

@@ -62,22 +68,33 @@ public class JdbcQueryMethod extends QueryMethod {
6268
private @Nullable RelationalEntityMetadata<?> metadata;
6369
private final boolean modifyingQuery;
6470

71+
private final SqlTypeResolver sqlTypeResolver;
72+
6573
// TODO: Remove NamedQueries and put it into JdbcQueryLookupStrategy
6674
public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
6775
NamedQueries namedQueries,
6876
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext) {
77+
this(method, metadata, factory, namedQueries, mappingContext, DefaultSqlTypeResolver.INSTANCE);
78+
}
79+
80+
public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
81+
NamedQueries namedQueries,
82+
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext,
83+
SqlTypeResolver sqlTypeResolver) {
6984

7085
super(method, metadata, factory);
7186
this.namedQueries = namedQueries;
7287
this.method = method;
7388
this.mappingContext = mappingContext;
7489
this.annotationCache = new ConcurrentReferenceHashMap<>();
7590
this.modifyingQuery = AnnotationUtils.findAnnotation(method, Modifying.class) != null;
91+
this.sqlTypeResolver = sqlTypeResolver;
7692
}
7793

94+
// SqlTypeResolver has to be wrapped, becuase the createParameters() is invoked in the parents constructor before child initialization
7895
@Override
7996
protected Parameters<?, ?> createParameters(ParametersSource parametersSource) {
80-
return new JdbcParameters(parametersSource);
97+
return new JdbcParameters(parametersSource, Lazy.of(() -> this.sqlTypeResolver));
8198
}
8299

83100
@Override

‎spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.data.jdbc.core.convert.JdbcConverter;
4040
import org.springframework.data.jdbc.core.mapping.JdbcValue;
4141
import org.springframework.data.jdbc.support.JdbcUtil;
42+
import org.springframework.data.relational.core.dialect.SqlTypeResolver;
4243
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
4344
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
4445
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
@@ -91,43 +92,6 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
9192
private final CachedResultSetExtractorFactory cachedResultSetExtractorFactory;
9293
private final ValueExpressionDelegate delegate;
9394

94-
/**
95-
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
96-
* and {@link RowMapper}.
97-
*
98-
* @param queryMethod must not be {@literal null}.
99-
* @param operations must not be {@literal null}.
100-
* @param defaultRowMapper can be {@literal null} (only in case of a modifying query).
101-
* @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead.
102-
*/
103-
@Deprecated(since = "3.4")
104-
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
105-
@Nullable RowMapper<?> defaultRowMapper, JdbcConverter converter,
106-
QueryMethodEvaluationContextProvider evaluationContextProvider) {
107-
this(queryMethod.getRequiredQuery(), queryMethod, operations, result -> (RowMapper<Object>) defaultRowMapper,
108-
converter, evaluationContextProvider);
109-
}
110-
111-
/**
112-
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
113-
* and {@link RowMapperFactory}.
114-
*
115-
* @param queryMethod must not be {@literal null}.
116-
* @param operations must not be {@literal null}.
117-
* @param rowMapperFactory must not be {@literal null}.
118-
* @param converter must not be {@literal null}.
119-
* @param evaluationContextProvider must not be {@literal null}.
120-
* @since 2.3
121-
* @deprecated use alternative constructor
122-
*/
123-
@Deprecated(since = "3.4")
124-
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
125-
RowMapperFactory rowMapperFactory, JdbcConverter converter,
126-
QueryMethodEvaluationContextProvider evaluationContextProvider) {
127-
this(queryMethod.getRequiredQuery(), queryMethod, operations, rowMapperFactory, converter,
128-
evaluationContextProvider);
129-
}
130-
13195
/**
13296
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
13397
* and {@link RowMapperFactory}.
@@ -197,28 +161,6 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara
197161
this.delegate = delegate;
198162
}
199163

200-
/**
201-
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
202-
* and {@link RowMapperFactory}.
203-
*
204-
* @param query must not be {@literal null} or empty.
205-
* @param queryMethod must not be {@literal null}.
206-
* @param operations must not be {@literal null}.
207-
* @param rowMapperFactory must not be {@literal null}.
208-
* @param converter must not be {@literal null}.
209-
* @param evaluationContextProvider must not be {@literal null}.
210-
* @since 3.4
211-
* @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead.
212-
*/
213-
@Deprecated(since = "3.4")
214-
public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
215-
RowMapperFactory rowMapperFactory, JdbcConverter converter,
216-
QueryMethodEvaluationContextProvider evaluationContextProvider) {
217-
this(query, queryMethod, operations, rowMapperFactory, converter, new CachingValueExpressionDelegate(
218-
new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), rootObject -> evaluationContextProvider
219-
.getEvaluationContext(queryMethod.getParameters(), new Object[] { rootObject })),
220-
ValueExpressionParser.create()));
221-
}
222164

223165
@Override
224166
public Object execute(Object[] objects) {

‎spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository
264264
*/
265265
JdbcQueryMethod getJdbcQueryMethod(Method method, RepositoryMetadata repositoryMetadata,
266266
ProjectionFactory projectionFactory, NamedQueries namedQueries) {
267-
return new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, getMappingContext());
267+
return new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, getMappingContext(), getDialect().getSqlTypeResolver());
268268
}
269269

270270
/**

‎spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,23 @@
2020
import static org.mockito.Mockito.*;
2121

2222
import java.lang.reflect.Method;
23+
import java.sql.JDBCType;
2324
import java.sql.ResultSet;
25+
import java.sql.SQLType;
26+
import java.sql.Types;
27+
import java.util.List;
2428
import java.util.Properties;
2529

30+
import org.assertj.core.api.Assertions;
2631
import org.junit.jupiter.api.BeforeEach;
2732
import org.junit.jupiter.api.Test;
2833
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
2934
import org.springframework.data.projection.ProjectionFactory;
35+
import org.springframework.data.relational.core.dialect.DefaultSqlTypeResolver;
36+
import org.springframework.data.relational.core.dialect.SqlTypeResolver;
3037
import org.springframework.data.relational.core.sql.LockMode;
3138
import org.springframework.data.relational.repository.Lock;
39+
import org.springframework.data.relational.repository.query.SqlType;
3240
import org.springframework.data.repository.core.NamedQueries;
3341
import org.springframework.data.repository.core.RepositoryMetadata;
3442
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
@@ -43,6 +51,7 @@
4351
* @author Moises Cisneros
4452
* @author Mark Paluch
4553
* @author Diego Krupitza
54+
* @author Mikhail Polivakha
4655
*/
4756
public class JdbcQueryMethodUnitTests {
4857

@@ -66,6 +75,8 @@ public void before() {
6675
namedQueries = new PropertiesBasedNamedQueries(properties);
6776

6877
metadata = mock(RepositoryMetadata.class);
78+
when(metadata.getDomainTypeInformation()).then(invocationOnMock -> TypeInformation.of(Object.class));
79+
6980
doReturn(String.class).when(metadata).getReturnedDomainClass(any(Method.class));
7081
doReturn(TypeInformation.of(String.class)).when(metadata).getReturnType(any(Method.class));
7182
}
@@ -78,6 +89,31 @@ public void returnsSqlStatement() throws NoSuchMethodException {
7889
assertThat(queryMethod.getDeclaredQuery()).isEqualTo(QUERY);
7990
}
8091

92+
@Test // DATAJDBC-165
93+
public void testSqlTypeResolver() throws NoSuchMethodException {
94+
95+
JdbcQueryMethod queryMethod = createJdbcQueryMethod(
96+
"findUserTestMethod",
97+
new DefaultSqlTypeResolver(),
98+
Integer.class, String.class, List.class
99+
);
100+
101+
JdbcParameters parameters = queryMethod.getParameters();
102+
103+
SQLType first = parameters.getParameter(0).getSqlType();
104+
SQLType second = parameters.getParameter(1).getSqlType();
105+
SQLType thirdActual = parameters.getParameter(2).getActualSqlType();
106+
107+
Assertions.assertThat(first.getName()).isEqualTo(JDBCType.TINYINT.getName());
108+
Assertions.assertThat(first.getVendorTypeNumber()).isEqualTo(Types.TINYINT);
109+
110+
Assertions.assertThat(second.getName()).isEqualTo(JDBCType.VARCHAR.getName());
111+
Assertions.assertThat(second.getVendorTypeNumber()).isEqualTo(Types.VARCHAR);
112+
113+
Assertions.assertThat(thirdActual.getName()).isEqualTo(JDBCType.SMALLINT.getName());
114+
Assertions.assertThat(thirdActual.getVendorTypeNumber()).isEqualTo(Types.SMALLINT);
115+
}
116+
81117
@Test // DATAJDBC-165
82118
public void returnsSpecifiedRowMapperClass() throws NoSuchMethodException {
83119

@@ -102,12 +138,6 @@ public void returnsSpecifiedSqlStatementIfNameAndValueAreGiven() throws NoSuchMe
102138

103139
}
104140

105-
private JdbcQueryMethod createJdbcQueryMethod(String methodName) throws NoSuchMethodException {
106-
107-
Method method = JdbcQueryMethodUnitTests.class.getDeclaredMethod(methodName);
108-
return new JdbcQueryMethod(method, metadata, mock(ProjectionFactory.class), namedQueries, mappingContext);
109-
}
110-
111141
@Test // DATAJDBC-234
112142
public void returnsImplicitlyNamedQuery() throws NoSuchMethodException {
113143

@@ -148,10 +178,27 @@ void returnsQueryMethodWithCorrectLockTypeNoLock() throws NoSuchMethodException
148178
assertThat(queryMethodWithWriteLock.lookupLockAnnotation()).isEmpty();
149179
}
150180

181+
private JdbcQueryMethod createJdbcQueryMethod(String methodName) throws NoSuchMethodException {
182+
return createJdbcQueryMethod(methodName, new DefaultSqlTypeResolver());
183+
}
184+
185+
private JdbcQueryMethod createJdbcQueryMethod(String methodName, SqlTypeResolver sqlTypeResolver, Class<?>... args) throws NoSuchMethodException {
186+
187+
Method method = JdbcQueryMethodUnitTests.class.getDeclaredMethod(methodName, args);
188+
return new JdbcQueryMethod(method, metadata, mock(ProjectionFactory.class), namedQueries, mappingContext, sqlTypeResolver);
189+
}
190+
151191
@Lock(LockMode.PESSIMISTIC_WRITE)
152192
@Query
153193
private void queryMethodWithWriteLock() {}
154194

195+
@Query
196+
private void findUserTestMethod(
197+
@SqlType(name = "TINYINT", vendorTypeNumber = Types.TINYINT) Integer age,
198+
String name,
199+
List<@SqlType(name = "SMALLINT", vendorTypeNumber = Types.SMALLINT) Integer> statuses
200+
) {}
201+
155202
@Lock(LockMode.PESSIMISTIC_READ)
156203
@Query
157204
private void queryMethodWithReadLock() {}

‎spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@
1919
import static org.assertj.core.api.SoftAssertions.*;
2020
import static org.mockito.Mockito.*;
2121

22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
2226
import java.lang.reflect.Method;
27+
import java.sql.JDBCType;
28+
import java.sql.SQLType;
2329
import java.util.Collection;
2430
import java.util.Collections;
2531
import java.util.Date;
@@ -35,14 +41,17 @@
3541
import org.springframework.data.jdbc.core.convert.RelationResolver;
3642
import org.springframework.data.jdbc.core.mapping.AggregateReference;
3743
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
44+
import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter;
3845
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
3946
import org.springframework.data.relational.core.dialect.Escaper;
4047
import org.springframework.data.relational.core.dialect.H2Dialect;
48+
import org.springframework.data.relational.core.dialect.SqlTypeResolver;
4149
import org.springframework.data.relational.core.mapping.Embedded;
4250
import org.springframework.data.relational.core.mapping.MappedCollection;
4351
import org.springframework.data.relational.core.mapping.Table;
4452
import org.springframework.data.relational.core.sql.LockMode;
4553
import org.springframework.data.relational.repository.Lock;
54+
import org.springframework.data.relational.repository.query.RelationalParameters;
4655
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
4756
import org.springframework.data.repository.NoRepositoryBean;
4857
import org.springframework.data.repository.Repository;
@@ -610,6 +619,18 @@ public void createsQueryWithLimitToFindEntitiesByStringAttribute() throws Except
610619
assertThat(query.getQuery()).isEqualTo(expectedSql);
611620
}
612621

622+
@Test // DATAJDBC-2020
623+
public void testCustomSqlTypeResolverApplied() throws Exception {
624+
625+
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameAndAgeIn", new TestSqlTypeResolver(), String.class, Collection.class);
626+
627+
JdbcParameter firstNameParam = queryMethod.getParameters().getParameter(0);
628+
JdbcParameter agesInParam = queryMethod.getParameters().getParameter(1);
629+
630+
assertThat(firstNameParam.getSqlType()).isEqualTo(JDBCType.CLOB);
631+
assertThat(agesInParam.getActualSqlType()).isEqualTo(JDBCType.TINYINT);
632+
}
633+
613634
@Test // DATAJDBC-318
614635
public void createsQueryToFindFirstEntityByStringAttribute() throws Exception {
615636

@@ -679,6 +700,12 @@ private JdbcQueryMethod getQueryMethod(String methodName, Class<?>... parameterT
679700
new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), mappingContext);
680701
}
681702

703+
private JdbcQueryMethod getQueryMethod(String methodName, SqlTypeResolver sqlTypeResolver, Class<?>... parameterTypes) throws Exception {
704+
Method method = UserRepository.class.getMethod(methodName, parameterTypes);
705+
return new JdbcQueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class),
706+
new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), mappingContext, sqlTypeResolver);
707+
}
708+
682709
private RelationalParametersParameterAccessor getAccessor(JdbcQueryMethod queryMethod, Object[] values) {
683710
return new RelationalParametersParameterAccessor(queryMethod, values);
684711
}
@@ -696,6 +723,8 @@ interface UserRepository extends Repository<User, Long> {
696723

697724
List<User> findAllByHated(Hobby hobby);
698725

726+
List<User> findAllByFirstNameAndAgeIn(String firstName, Collection<Integer> ages);
727+
699728
List<User> findAllByHatedName(String name);
700729

701730
List<User> findAllByHobbies(Object hobbies);
@@ -775,6 +804,35 @@ interface UserRepository extends Repository<User, Long> {
775804
long countByFirstName(String name);
776805
}
777806

807+
@Target(ElementType.PARAMETER)
808+
@Retention(RetentionPolicy.RUNTIME)
809+
@interface MySqlType { }
810+
811+
@Target(ElementType.PARAMETER)
812+
@Retention(RetentionPolicy.RUNTIME)
813+
@interface MyActualSqlType { }
814+
815+
static class TestSqlTypeResolver implements SqlTypeResolver {
816+
817+
@Override
818+
public SQLType resolveSqlType(RelationalParameters.RelationalParameter relationalParameter) {
819+
if (relationalParameter.getMethodParameter().hasParameterAnnotation(MySqlType.class)) {
820+
return JDBCType.CLOB;
821+
} else {
822+
return null;
823+
}
824+
}
825+
826+
@Override
827+
public SQLType resolveActualSqlType(RelationalParameters.RelationalParameter relationalParameter) {
828+
if (relationalParameter.getMethodParameter().hasParameterAnnotation(MyActualSqlType.class)) {
829+
return JDBCType.TINYINT;
830+
} else {
831+
return null;
832+
}
833+
}
834+
}
835+
778836
@Table("users")
779837
static class User {
780838

‎spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.mockito.Mockito.*;
2020

21+
import java.lang.annotation.ElementType;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
2125
import java.lang.reflect.Method;
2226
import java.sql.JDBCType;
2327
import java.sql.ResultSet;
28+
import java.sql.SQLType;
2429
import java.util.ArrayList;
2530
import java.util.Arrays;
31+
import java.util.Collection;
2632
import java.util.Iterator;
2733
import java.util.List;
2834
import java.util.Properties;
@@ -50,9 +56,12 @@
5056
import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
5157
import org.springframework.data.jdbc.core.convert.RelationResolver;
5258
import org.springframework.data.jdbc.core.mapping.JdbcValue;
59+
import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter;
5360
import org.springframework.data.jdbc.support.JdbcUtil;
5461
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
62+
import org.springframework.data.relational.core.dialect.SqlTypeResolver;
5563
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
64+
import org.springframework.data.relational.repository.query.RelationalParameters;
5665
import org.springframework.data.repository.Repository;
5766
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
5867
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
@@ -278,6 +287,23 @@ void limitNotSupported() {
278287
.isInstanceOf(UnsupportedOperationException.class);
279288
}
280289

290+
@Test // GH-2020
291+
void testCustomSqlTypeResolution() {
292+
293+
JdbcQueryMethod queryMethod = createMethod("findByWithSqlTypeResolver", new TestSqlTypResolver(), Integer.class, Collection.class);
294+
295+
StringBasedJdbcQuery stringBasedJdbcQuery = new StringBasedJdbcQuery(queryMethod, operations,
296+
result -> defaultRowMapper, converter, delegate);
297+
298+
JdbcParameters parameters = stringBasedJdbcQuery.getQueryMethod().getParameters();
299+
300+
JdbcParameter tinyInt = parameters.getParameter(0);
301+
JdbcParameter smallIntActual = parameters.getParameter(1);
302+
303+
assertThat(tinyInt.getSqlType()).isEqualTo(JDBCType.TINYINT);
304+
assertThat(smallIntActual.getActualSqlType()).isEqualTo(JDBCType.SMALLINT);
305+
}
306+
281307
@Test // GH-1212
282308
void convertsEnumCollectionParameterIntoStringCollectionParameter() {
283309

@@ -433,6 +459,13 @@ private JdbcQueryMethod createMethod(String methodName, Class<?>... paramTypes)
433459
new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), this.context);
434460
}
435461

462+
private JdbcQueryMethod createMethod(String methodName, SqlTypeResolver sqlTypeResolver, Class<?>... paramTypes) {
463+
464+
Method method = ReflectionUtils.findMethod(MyRepository.class, methodName, paramTypes);
465+
return new JdbcQueryMethod(method, new DefaultRepositoryMetadata(MyRepository.class),
466+
new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), this.context, sqlTypeResolver);
467+
}
468+
436469
private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod) {
437470
return createQuery(queryMethod, null, null);
438471
}
@@ -497,6 +530,9 @@ interface MyRepository extends Repository<Object, Long> {
497530
@Query(value = "some sql statement")
498531
List<Object> findByListContainer(ListContainer value);
499532

533+
@Query(value = "SELECT * FROM my_table WHERE t = ? AND f IN (?)")
534+
List<Object> findByWithSqlTypeResolver(@MySqlType Integer tinyeInt, @MyActualSqlType Collection<Integer> smallInt);
535+
500536
@Query("SELECT * FROM table WHERE c = :#{myext.testValue} AND c2 = :#{myext.doSomething()}")
501537
Object findBySpelExpression(Object object);
502538

@@ -507,6 +543,35 @@ interface MyRepository extends Repository<Object, Long> {
507543
Object findByListOfTuples(@Param("tuples") List<Object[]> tuples);
508544
}
509545

546+
@Target(ElementType.PARAMETER)
547+
@Retention(RetentionPolicy.RUNTIME)
548+
@interface MySqlType { }
549+
550+
@Target(ElementType.PARAMETER)
551+
@Retention(RetentionPolicy.RUNTIME)
552+
@interface MyActualSqlType { }
553+
554+
static class TestSqlTypResolver implements SqlTypeResolver {
555+
556+
@Override
557+
public SQLType resolveSqlType(RelationalParameters.RelationalParameter relationalParameter) {
558+
if (relationalParameter.getMethodParameter().hasParameterAnnotation(MySqlType.class)) {
559+
return JDBCType.TINYINT;
560+
} else {
561+
return null;
562+
}
563+
}
564+
565+
@Override
566+
public SQLType resolveActualSqlType(RelationalParameters.RelationalParameter relationalParameter) {
567+
if (relationalParameter.getMethodParameter().hasParameterAnnotation(MyActualSqlType.class)) {
568+
return JDBCType.SMALLINT;
569+
} else {
570+
return null;
571+
}
572+
}
573+
}
574+
510575
private static class CustomRowMapper implements RowMapper<Object> {
511576

512577
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.relational.core.dialect;
18+
19+
import java.lang.reflect.AnnotatedParameterizedType;
20+
import java.lang.reflect.AnnotatedType;
21+
import java.lang.reflect.Parameter;
22+
import java.sql.SQLType;
23+
24+
import org.springframework.core.MethodParameter;
25+
import org.springframework.data.relational.repository.query.SqlType;
26+
import org.springframework.data.relational.repository.query.RelationalParameters;
27+
import org.springframework.data.util.TypeInformation;
28+
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
30+
31+
/**
32+
* Default implementation of {@link SqlTypeResolver}. Capable to resolve the {@link SqlType} annotation
33+
* on the {@link java.lang.annotation.ElementType#TYPE_USE}, like this:
34+
* <p>
35+
* <pre class="code">
36+
* List<User> findByAge(&#64;SqlType(name = "TINYINT", vendorTypeNumber = Types.TINYINT) byte age);
37+
* </pre>
38+
*
39+
* Or, if the intention is to specify the actual {@link SQLType}, then the following needs to be done:
40+
* <pre class="code">
41+
* List<User> findByAgeIn(List<&#64;SqlType(name = "TINYINT", vendorTypeNumber = Types.TINYINT) Integer> age);
42+
* </pre>
43+
*
44+
* @author Mikhail Polivakha
45+
*/
46+
public class DefaultSqlTypeResolver implements SqlTypeResolver {
47+
48+
public static DefaultSqlTypeResolver INSTANCE = new DefaultSqlTypeResolver();
49+
50+
@Override
51+
@Nullable
52+
public SQLType resolveSqlType(RelationalParameters.RelationalParameter relationalParameter) {
53+
SqlType parameterAnnotation = relationalParameter.getMethodParameter().getParameterAnnotation(SqlType.class);
54+
55+
if (parameterAnnotation != null) {
56+
return new AnnotationBasedSqlType(parameterAnnotation);
57+
} else {
58+
return null;
59+
}
60+
}
61+
62+
@Override
63+
@Nullable
64+
public SQLType resolveActualSqlType(RelationalParameters.RelationalParameter relationalParameter) {
65+
MethodParameter methodParameter = relationalParameter.getMethodParameter();
66+
67+
TypeInformation<?> typeOfParameter = TypeInformation.of(methodParameter.getParameterType());
68+
69+
if (typeOfParameter.isCollectionLike()) {
70+
Parameter parameter = methodParameter.getParameter();
71+
AnnotatedType annotatedType = parameter.getAnnotatedType();
72+
73+
if (annotatedType instanceof AnnotatedParameterizedType parameterizedType) {
74+
return searchForGenericTypeUseAnnotation(parameterizedType);
75+
}
76+
}
77+
78+
return null;
79+
}
80+
81+
@Nullable
82+
private static AnnotationBasedSqlType searchForGenericTypeUseAnnotation(AnnotatedParameterizedType parameterizedType) {
83+
AnnotatedType[] annotatedArguments = parameterizedType.getAnnotatedActualTypeArguments();
84+
85+
if (annotatedArguments.length != 1) {
86+
return null;
87+
}
88+
89+
SqlType typeUseSqlTypeAnnotation = annotatedArguments[0].getAnnotation(SqlType.class);
90+
91+
if (typeUseSqlTypeAnnotation != null) {
92+
return new AnnotationBasedSqlType(typeUseSqlTypeAnnotation);
93+
} else {
94+
return null;
95+
}
96+
}
97+
98+
/**
99+
* {@link SQLType} determined from the {@link SqlType} annotation.
100+
*
101+
* @author Mikhail Polivakha
102+
*/
103+
protected static class AnnotationBasedSqlType implements SQLType {
104+
105+
private final SqlType sqlType;
106+
107+
public AnnotationBasedSqlType(SqlType sqlType) {
108+
Assert.notNull(sqlType, "sqlType must not be null");
109+
110+
this.sqlType = sqlType;
111+
}
112+
113+
@Override
114+
public String getName() {
115+
return sqlType.name();
116+
}
117+
118+
@Override
119+
public String getVendor() {
120+
return "Spring Data JDBC";
121+
}
122+
123+
@Override
124+
public Integer getVendorTypeNumber() {
125+
return sqlType.vendorTypeNumber();
126+
}
127+
}
128+
}

‎spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,13 @@ default SimpleFunction getExistsFunction() {
147147
default boolean supportsSingleQueryLoading() {
148148
return true;
149149
}
150+
151+
/**
152+
* Returns a {@link SqlTypeResolver} of this dialect.
153+
*
154+
* @since 4.0
155+
*/
156+
default SqlTypeResolver getSqlTypeResolver() {
157+
return DefaultSqlTypeResolver.INSTANCE;
158+
}
150159
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.relational.core.dialect;
18+
19+
import java.sql.SQLType;
20+
21+
import org.springframework.data.relational.repository.query.RelationalParameters.RelationalParameter;
22+
import org.springframework.data.util.TypeInformation;
23+
import org.springframework.lang.Nullable;
24+
25+
/**
26+
* Common interface for all objects capable to resolve the {@link SQLType} to be used for a give method parameter.
27+
*
28+
* @author Mikhail Polivakha
29+
*/
30+
public interface SqlTypeResolver {
31+
32+
/**
33+
* Resolving the {@link SQLType} from the given {@link RelationalParameter}.
34+
*
35+
* @param relationalParameter the parameter of the query method
36+
* @return {@code null} in case the given {@link SqlTypeResolver} cannot or do not want to determine the
37+
* {@link SQLType} of the given parameter
38+
*/
39+
@Nullable
40+
SQLType resolveSqlType(RelationalParameter relationalParameter);
41+
42+
/**
43+
* Resolving the {@link SQLType} from the given {@link RelationalParameter}. The definition of "actual"
44+
* type can be looked up in the {@link TypeInformation#getActualType()}.
45+
*
46+
* @param relationalParameter the parameter of the query method
47+
* @return {@code null} in case the given {@link SqlTypeResolver} cannot or do not want to determine the
48+
* actual {@link SQLType} of the given parameter
49+
*/
50+
@Nullable
51+
SQLType resolveActualSqlType(RelationalParameter relationalParameter);
52+
}

‎spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ protected RelationalParameters createFrom(List<RelationalParameter> parameters)
6666
public static class RelationalParameter extends Parameter {
6767

6868
private final TypeInformation<?> typeInformation;
69+
private final MethodParameter methodParameter;
6970

7071
/**
7172
* Creates a new {@link RelationalParameter}.
@@ -75,7 +76,7 @@ public static class RelationalParameter extends Parameter {
7576
protected RelationalParameter(MethodParameter parameter, TypeInformation<?> domainType) {
7677
super(parameter, domainType);
7778
this.typeInformation = TypeInformation.fromMethodParameter(parameter);
78-
79+
this.methodParameter = parameter;
7980
}
8081

8182
public ResolvableType getResolvableType() {
@@ -85,5 +86,9 @@ public ResolvableType getResolvableType() {
8586
public TypeInformation<?> getTypeInformation() {
8687
return typeInformation;
8788
}
89+
90+
public MethodParameter getMethodParameter() {
91+
return methodParameter;
92+
}
8893
}
8994
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.relational.repository.query;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.sql.SQLType;
25+
26+
import org.springframework.data.relational.core.dialect.DefaultSqlTypeResolver;
27+
28+
/**
29+
* Serves as a hint to the {@link DefaultSqlTypeResolver}, that signals the {@link java.sql.SQLType} to be used.
30+
* The arguments of this annotation are identical to the methods on {@link java.sql.SQLType} interface, expect for
31+
* the {@link SQLType#getVendor()}, which is absent, because it typically does not matter as such for the underlying
32+
* JDBC drivers. For examples of usage, take a look onto {@link DefaultSqlTypeResolver}.
33+
*
34+
* @see DefaultSqlTypeResolver
35+
* @author Mikhail Polivakha
36+
*/
37+
@Documented
38+
@Target({ElementType.PARAMETER, ElementType.TYPE_USE})
39+
@Retention(RetentionPolicy.RUNTIME)
40+
public @interface SqlType {
41+
42+
/**
43+
* Returns the {@code SQLType} name that represents a SQL data type.
44+
*
45+
* @return The name of this {@code SQLType}.
46+
*/
47+
String name();
48+
49+
/**
50+
* Returns the vendor specific type number for the data type.
51+
*
52+
* @return An Integer representing the vendor specific data type
53+
*/
54+
int vendorTypeNumber();
55+
}

0 commit comments

Comments
 (0)
Please sign in to comment.