Browse Source

JdbcClient holds ConversionService for queries with mapped classes

Closes gh-33467
pull/34636/head
Juergen Hoeller 9 months ago
parent
commit
0e1422820f
  1. 18
      spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java
  2. 22
      spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java
  3. 20
      spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java
  4. 24
      spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java

18
spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-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.
@ -70,6 +70,19 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> { @@ -70,6 +70,19 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> {
}
}
/**
* Create a new {@code SingleColumnRowMapper}.
* @param requiredType the type that each result object is expected to match
* @param conversionService a {@link ConversionService} for converting a fetched value
* @since 7.0
*/
public SingleColumnRowMapper(Class<T> requiredType, @Nullable ConversionService conversionService) {
if (requiredType != Object.class) {
setRequiredType(requiredType);
}
setConversionService(conversionService);
}
/**
* Set the type that each result object is expected to match.
@ -84,12 +97,13 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> { @@ -84,12 +97,13 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> {
* Set a {@link ConversionService} for converting a fetched value.
* <p>Default is the {@link DefaultConversionService}.
* @since 5.0.4
* @see DefaultConversionService#getSharedInstance
* @see DefaultConversionService#getSharedInstance()
*/
public void setConversionService(@Nullable ConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Extract a value for the single column in the current row.
* <p>Validates that there is only one column selected,

22
spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-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.
@ -28,6 +28,8 @@ import javax.sql.DataSource; @@ -28,6 +28,8 @@ import javax.sql.DataSource;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
@ -64,24 +66,25 @@ final class DefaultJdbcClient implements JdbcClient { @@ -64,24 +66,25 @@ final class DefaultJdbcClient implements JdbcClient {
private final NamedParameterJdbcOperations namedParamOps;
private final ConversionService conversionService;
private final Map<Class<?>, RowMapper<?>> rowMapperCache = new ConcurrentHashMap<>();
public DefaultJdbcClient(DataSource dataSource) {
this.classicOps = new JdbcTemplate(dataSource);
this.namedParamOps = new NamedParameterJdbcTemplate(this.classicOps);
this(new JdbcTemplate(dataSource));
}
public DefaultJdbcClient(JdbcOperations jdbcTemplate) {
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null");
this.classicOps = jdbcTemplate;
this.namedParamOps = new NamedParameterJdbcTemplate(jdbcTemplate);
this(new NamedParameterJdbcTemplate(jdbcTemplate), null);
}
public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate) {
public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate, @Nullable ConversionService conversionService) {
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null");
this.classicOps = jdbcTemplate.getJdbcOperations();
this.namedParamOps = jdbcTemplate;
this.conversionService =
(conversionService != null ? conversionService : DefaultConversionService.getSharedInstance());
}
@ -201,8 +204,9 @@ final class DefaultJdbcClient implements JdbcClient { @@ -201,8 +204,9 @@ final class DefaultJdbcClient implements JdbcClient {
@Override
public <T> MappedQuerySpec<T> query(Class<T> mappedClass) {
RowMapper<?> rowMapper = rowMapperCache.computeIfAbsent(mappedClass, key ->
BeanUtils.isSimpleProperty(mappedClass) ? new SingleColumnRowMapper<>(mappedClass) :
new SimplePropertyRowMapper<>(mappedClass));
BeanUtils.isSimpleProperty(mappedClass) ?
new SingleColumnRowMapper<>(mappedClass, conversionService) :
new SimplePropertyRowMapper<>(mappedClass, conversionService));
return query((RowMapper<T>) rowMapper);
}

20
spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-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.
@ -28,6 +28,7 @@ import javax.sql.DataSource; @@ -28,6 +28,7 @@ import javax.sql.DataSource;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.ResultSetExtractor;
@ -107,7 +108,22 @@ public interface JdbcClient { @@ -107,7 +108,22 @@ public interface JdbcClient {
* @param jdbcTemplate the delegate to perform operations on
*/
static JdbcClient create(NamedParameterJdbcOperations jdbcTemplate) {
return new DefaultJdbcClient(jdbcTemplate);
return new DefaultJdbcClient(jdbcTemplate, null);
}
/**
* Create a {@code JdbcClient} for the given {@link NamedParameterJdbcOperations} delegate,
* typically an {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}.
* <p>Use this factory method to reuse existing {@code NamedParameterJdbcTemplate}
* configuration, including its underlying {@code JdbcTemplate} and {@code DataSource},
* along with a custom {@link ConversionService} for queries with mapped classes.
* @param jdbcTemplate the delegate to perform operations on
* @param conversionService a {@link ConversionService} for converting fetched JDBC values
* to mapped classes in {@link StatementSpec#query(Class)}
* @since 7.0
*/
static JdbcClient create(NamedParameterJdbcOperations jdbcTemplate, ConversionService conversionService) {
return new DefaultJdbcClient(jdbcTemplate, conversionService);
}

24
spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java

@ -16,10 +16,12 @@ @@ -16,10 +16,12 @@
package org.springframework.jdbc.core.simple;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
@ -33,6 +35,11 @@ import javax.sql.DataSource; @@ -33,6 +35,11 @@ import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.util.NumberUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.anyString;
@ -593,13 +600,22 @@ class JdbcClientQueryTests { @@ -593,13 +600,22 @@ class JdbcClientQueryTests {
@Test
void queryForMappedFieldHolderWithNamedParam() throws Exception {
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(22);
given(resultSet.getObject(1, BigInteger.class)).willThrow(new SQLFeatureNotSupportedException());
given(resultSet.getObject(1)).willReturn("big22");
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(new Converter<String, BigInteger>() { // explicit for generics
@Override
public BigInteger convert(String source) {
return NumberUtils.parseNumber(source.substring(3), BigInteger.class);
}
});
client = JdbcClient.create(new NamedParameterJdbcTemplate(dataSource), conversionService);
AgeFieldHolder value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.param("id", 3)
.query(AgeFieldHolder.class).single();
assertThat(value.age).isEqualTo(22);
assertThat(value.age).isEqualTo(BigInteger.valueOf(22));
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
verify(preparedStatement).setObject(1, 3);
verify(resultSet).close();
@ -656,7 +672,7 @@ class JdbcClientQueryTests { @@ -656,7 +672,7 @@ class JdbcClientQueryTests {
static class AgeFieldHolder {
public int age;
public BigInteger age;
}
}

Loading…
Cancel
Save