Skip to content

Commit 0e14228

Browse files
committed
JdbcClient holds ConversionService for queries with mapped classes
Closes gh-33467
1 parent a145f62 commit 0e14228

File tree

4 files changed

+67
-17
lines changed

4 files changed

+67
-17
lines changed

Diff for: spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -70,6 +70,19 @@ public SingleColumnRowMapper(Class<T> requiredType) {
7070
}
7171
}
7272

73+
/**
74+
* Create a new {@code SingleColumnRowMapper}.
75+
* @param requiredType the type that each result object is expected to match
76+
* @param conversionService a {@link ConversionService} for converting a fetched value
77+
* @since 7.0
78+
*/
79+
public SingleColumnRowMapper(Class<T> requiredType, @Nullable ConversionService conversionService) {
80+
if (requiredType != Object.class) {
81+
setRequiredType(requiredType);
82+
}
83+
setConversionService(conversionService);
84+
}
85+
7386

7487
/**
7588
* Set the type that each result object is expected to match.
@@ -84,12 +97,13 @@ public void setRequiredType(Class<T> requiredType) {
8497
* Set a {@link ConversionService} for converting a fetched value.
8598
* <p>Default is the {@link DefaultConversionService}.
8699
* @since 5.0.4
87-
* @see DefaultConversionService#getSharedInstance
100+
* @see DefaultConversionService#getSharedInstance()
88101
*/
89102
public void setConversionService(@Nullable ConversionService conversionService) {
90103
this.conversionService = conversionService;
91104
}
92105

106+
93107
/**
94108
* Extract a value for the single column in the current row.
95109
* <p>Validates that there is only one column selected,

Diff for: spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,8 @@
2828
import org.jspecify.annotations.Nullable;
2929

3030
import org.springframework.beans.BeanUtils;
31+
import org.springframework.core.convert.ConversionService;
32+
import org.springframework.core.convert.support.DefaultConversionService;
3133
import org.springframework.jdbc.core.JdbcOperations;
3234
import org.springframework.jdbc.core.JdbcTemplate;
3335
import org.springframework.jdbc.core.PreparedStatementCreator;
@@ -64,24 +66,25 @@ final class DefaultJdbcClient implements JdbcClient {
6466

6567
private final NamedParameterJdbcOperations namedParamOps;
6668

69+
private final ConversionService conversionService;
70+
6771
private final Map<Class<?>, RowMapper<?>> rowMapperCache = new ConcurrentHashMap<>();
6872

6973

7074
public DefaultJdbcClient(DataSource dataSource) {
71-
this.classicOps = new JdbcTemplate(dataSource);
72-
this.namedParamOps = new NamedParameterJdbcTemplate(this.classicOps);
75+
this(new JdbcTemplate(dataSource));
7376
}
7477

7578
public DefaultJdbcClient(JdbcOperations jdbcTemplate) {
76-
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null");
77-
this.classicOps = jdbcTemplate;
78-
this.namedParamOps = new NamedParameterJdbcTemplate(jdbcTemplate);
79+
this(new NamedParameterJdbcTemplate(jdbcTemplate), null);
7980
}
8081

81-
public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate) {
82+
public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate, @Nullable ConversionService conversionService) {
8283
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null");
8384
this.classicOps = jdbcTemplate.getJdbcOperations();
8485
this.namedParamOps = jdbcTemplate;
86+
this.conversionService =
87+
(conversionService != null ? conversionService : DefaultConversionService.getSharedInstance());
8588
}
8689

8790

@@ -201,8 +204,9 @@ public ResultQuerySpec query() {
201204
@Override
202205
public <T> MappedQuerySpec<T> query(Class<T> mappedClass) {
203206
RowMapper<?> rowMapper = rowMapperCache.computeIfAbsent(mappedClass, key ->
204-
BeanUtils.isSimpleProperty(mappedClass) ? new SingleColumnRowMapper<>(mappedClass) :
205-
new SimplePropertyRowMapper<>(mappedClass));
207+
BeanUtils.isSimpleProperty(mappedClass) ?
208+
new SingleColumnRowMapper<>(mappedClass, conversionService) :
209+
new SimplePropertyRowMapper<>(mappedClass, conversionService));
206210
return query((RowMapper<T>) rowMapper);
207211
}
208212

Diff for: spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828

2929
import org.jspecify.annotations.Nullable;
3030

31+
import org.springframework.core.convert.ConversionService;
3132
import org.springframework.dao.support.DataAccessUtils;
3233
import org.springframework.jdbc.core.JdbcOperations;
3334
import org.springframework.jdbc.core.ResultSetExtractor;
@@ -107,7 +108,22 @@ static JdbcClient create(JdbcOperations jdbcTemplate) {
107108
* @param jdbcTemplate the delegate to perform operations on
108109
*/
109110
static JdbcClient create(NamedParameterJdbcOperations jdbcTemplate) {
110-
return new DefaultJdbcClient(jdbcTemplate);
111+
return new DefaultJdbcClient(jdbcTemplate, null);
112+
}
113+
114+
/**
115+
* Create a {@code JdbcClient} for the given {@link NamedParameterJdbcOperations} delegate,
116+
* typically an {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}.
117+
* <p>Use this factory method to reuse existing {@code NamedParameterJdbcTemplate}
118+
* configuration, including its underlying {@code JdbcTemplate} and {@code DataSource},
119+
* along with a custom {@link ConversionService} for queries with mapped classes.
120+
* @param jdbcTemplate the delegate to perform operations on
121+
* @param conversionService a {@link ConversionService} for converting fetched JDBC values
122+
* to mapped classes in {@link StatementSpec#query(Class)}
123+
* @since 7.0
124+
*/
125+
static JdbcClient create(NamedParameterJdbcOperations jdbcTemplate, ConversionService conversionService) {
126+
return new DefaultJdbcClient(jdbcTemplate, conversionService);
111127
}
112128

113129

Diff for: spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java

+20-4
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package org.springframework.jdbc.core.simple;
1818

19+
import java.math.BigInteger;
1920
import java.sql.Connection;
2021
import java.sql.PreparedStatement;
2122
import java.sql.ResultSet;
2223
import java.sql.ResultSetMetaData;
24+
import java.sql.SQLFeatureNotSupportedException;
2325
import java.sql.Types;
2426
import java.util.ArrayList;
2527
import java.util.Arrays;
@@ -33,6 +35,11 @@
3335
import org.junit.jupiter.api.BeforeEach;
3436
import org.junit.jupiter.api.Test;
3537

38+
import org.springframework.core.convert.converter.Converter;
39+
import org.springframework.core.convert.support.GenericConversionService;
40+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
41+
import org.springframework.util.NumberUtils;
42+
3643
import static org.assertj.core.api.Assertions.assertThat;
3744
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3845
import static org.mockito.ArgumentMatchers.anyString;
@@ -593,13 +600,22 @@ void queryForMappedRecordWithNamedParam() throws Exception {
593600
@Test
594601
void queryForMappedFieldHolderWithNamedParam() throws Exception {
595602
given(resultSet.next()).willReturn(true, false);
596-
given(resultSet.getInt(1)).willReturn(22);
597-
603+
given(resultSet.getObject(1, BigInteger.class)).willThrow(new SQLFeatureNotSupportedException());
604+
given(resultSet.getObject(1)).willReturn("big22");
605+
606+
GenericConversionService conversionService = new GenericConversionService();
607+
conversionService.addConverter(new Converter<String, BigInteger>() { // explicit for generics
608+
@Override
609+
public BigInteger convert(String source) {
610+
return NumberUtils.parseNumber(source.substring(3), BigInteger.class);
611+
}
612+
});
613+
client = JdbcClient.create(new NamedParameterJdbcTemplate(dataSource), conversionService);
598614
AgeFieldHolder value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
599615
.param("id", 3)
600616
.query(AgeFieldHolder.class).single();
601617

602-
assertThat(value.age).isEqualTo(22);
618+
assertThat(value.age).isEqualTo(BigInteger.valueOf(22));
603619
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
604620
verify(preparedStatement).setObject(1, 3);
605621
verify(resultSet).close();
@@ -656,7 +672,7 @@ record AgeRecord(int age) {
656672

657673
static class AgeFieldHolder {
658674

659-
public int age;
675+
public BigInteger age;
660676
}
661677

662678
}

0 commit comments

Comments
 (0)