Browse Source

Introduce SimplePropertyRowMapper with flexible constructor/property/field mapping

Includes query(Class) method with value and property mapping support on JdbcClient.
JdbcClient's singleColumn/singleValue are declared without a Class parameter now.

Closes gh-26594
See gh-30931
pull/31063/head
Juergen Hoeller 2 years ago
parent
commit
d89e305c87
  1. 25
      spring-core/src/main/java/org/springframework/util/ReflectionUtils.java
  2. 23
      spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java
  3. 1
      spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java
  4. 213
      spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.java
  5. 7
      spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java
  6. 25
      spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java
  7. 25
      spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java
  8. 23
      spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java
  9. 70
      spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java
  10. 1
      spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java
  11. 2
      spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java
  12. 165
      spring-jdbc/src/test/java/org/springframework/jdbc/core/SimplePropertyRowMapperTests.java
  13. 111
      spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java
  14. 10
      spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java
  15. 11
      spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java

25
spring-core/src/main/java/org/springframework/util/ReflectionUtils.java

@ -609,6 +609,31 @@ public abstract class ReflectionUtils { @@ -609,6 +609,31 @@ public abstract class ReflectionUtils {
return null;
}
/**
* Attempt to find a {@link Field field} on the supplied {@link Class} with the
* supplied {@code name}. Searches all superclasses up to {@link Object}.
* @param clazz the class to introspect
* @param name the name of the field (with upper/lower case to be ignored)
* @return the corresponding Field object, or {@code null} if not found
* @since 6.1
*/
@Nullable
public static Field findFieldIgnoreCase(Class<?> clazz, String name) {
Assert.notNull(clazz, "Class must not be null");
Assert.notNull(name, "Name must not be null");
Class<?> searchType = clazz;
while (Object.class != searchType && searchType != null) {
Field[] fields = getDeclaredFields(searchType);
for (Field field : fields) {
if (name.equalsIgnoreCase(field.getName())) {
return field;
}
}
searchType = searchType.getSuperclass();
}
return null;
}
/**
* Set the field represented by the supplied {@linkplain Field field object} on
* the specified {@linkplain Object target object} to the specified {@code value}.

23
spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java

@ -61,7 +61,7 @@ import org.springframework.util.StringUtils; @@ -61,7 +61,7 @@ import org.springframework.util.StringUtils;
* long, Long, float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc.
*
* <p>To facilitate mapping between columns and properties that don't have matching
* names, try using column aliases in the SQL statement like
* names, try using underscore-separated column aliases in the SQL statement like
* {@code "select fname as first_name from customer"}, where {@code first_name}
* can be mapped to a {@code setFirstName(String)} method in the target class.
*
@ -87,6 +87,7 @@ import org.springframework.util.StringUtils; @@ -87,6 +87,7 @@ import org.springframework.util.StringUtils;
* @since 2.5
* @param <T> the result type
* @see DataClassRowMapper
* @see SimplePropertyRowMapper
*/
public class BeanPropertyRowMapper<T> implements RowMapper<T> {
@ -278,6 +279,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> { @@ -278,6 +279,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
* @param name the original name
* @return the converted name
* @since 4.2
* @see #underscoreName
*/
protected String lowerCaseName(String name) {
return name.toLowerCase(Locale.US);
@ -289,25 +291,10 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> { @@ -289,25 +291,10 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
* @param name the original name
* @return the converted name
* @since 4.2
* @see #lowerCaseName
* @see JdbcUtils#convertPropertyNameToUnderscoreName
*/
protected String underscoreName(String name) {
if (!StringUtils.hasLength(name)) {
return "";
}
StringBuilder result = new StringBuilder();
result.append(Character.toLowerCase(name.charAt(0)));
for (int i = 1; i < name.length(); i++) {
char c = name.charAt(i);
if (Character.isUpperCase(c)) {
result.append('_').append(Character.toLowerCase(c));
}
else {
result.append(c);
}
}
return result.toString();
return JdbcUtils.convertPropertyNameToUnderscoreName(name);
}

1
spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java

@ -57,6 +57,7 @@ import org.springframework.util.Assert; @@ -57,6 +57,7 @@ import org.springframework.util.Assert;
* @author Sam Brannen
* @since 5.3
* @param <T> the result type
* @see SimplePropertyRowMapper
*/
public class DataClassRowMapper<T> extends BeanPropertyRowMapper<T> {

213
spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.java

@ -0,0 +1,213 @@ @@ -0,0 +1,213 @@
/*
* Copyright 2002-2023 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.jdbc.core;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* {@link RowMapper} implementation that converts a row into a new instance
* of the specified mapped target class. The mapped target class must be a
* top-level class or {@code static} nested class, and it may expose either a
* <em>data class</em> constructor with named parameters corresponding to column
* names or classic bean property setter methods with property names corresponding
* to column names or fields with corresponding field names.
*
* <p>When combining a data class constructor with setter methods, any property
* mapped successfully via a constructor argument will not be mapped additionally
* via a corresponding setter method or field mapping. This means that constructor
* arguments take precedence over property setter methods which in turn take
* precedence over direct field mappings.
*
* <p>To facilitate mapping between columns and properties that don't have matching
* names, try using underscore-separated column aliases in the SQL statement like
* {@code "select fname as first_name from customer"}, where {@code first_name}
* can be mapped to a {@code setFirstName(String)} method in the target class.
*
* <p>This is a flexible alternative to {@link DataClassRowMapper} and
* {@link BeanPropertyRowMapper} for scenarios where no specific customization
* and no pre-defined property mappings are needed.
*
* <p>In terms of its fallback property discovery algorithm, this class is similar to
* {@link org.springframework.jdbc.core.namedparam.SimplePropertySqlParameterSource}
* and is similarly used for {@link org.springframework.jdbc.core.simple.JdbcClient}.
*
* @author Juergen Hoeller
* @since 6.1
* @param <T> the result type
* @see DataClassRowMapper
* @see BeanPropertyRowMapper
* @see org.springframework.jdbc.core.simple.JdbcClient.StatementSpec#query(Class)
* @see org.springframework.jdbc.core.namedparam.SimplePropertySqlParameterSource
*/
public class SimplePropertyRowMapper<T> implements RowMapper<T> {
private static final Object NO_DESCRIPTOR = new Object();
private final Class<T> mappedClass;
private final ConversionService conversionService;
private final Constructor<T> mappedConstructor;
private final String[] constructorParameterNames;
private final TypeDescriptor[] constructorParameterTypes;
private final Map<String, Object> propertyDescriptors = new ConcurrentHashMap<>();
/**
* Create a new {@code SimplePropertyRowMapper}.
* @param mappedClass the class that each row should be mapped to
*/
public SimplePropertyRowMapper(Class<T> mappedClass) {
this(mappedClass, DefaultConversionService.getSharedInstance());
}
/**
* Create a new {@code SimplePropertyRowMapper}.
* @param mappedClass the class that each row should be mapped to
* @param conversionService a {@link ConversionService} for binding
* JDBC values to bean properties
*/
public SimplePropertyRowMapper(Class<T> mappedClass, ConversionService conversionService) {
Assert.notNull(mappedClass, "Mapped Class must not be null");
Assert.notNull(conversionService, "ConversionService must not be null");
this.mappedClass = mappedClass;
this.conversionService = conversionService;
this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass);
int paramCount = this.mappedConstructor.getParameterCount();
this.constructorParameterNames = (paramCount > 0 ?
BeanUtils.getParameterNames(this.mappedConstructor) : new String[0]);
this.constructorParameterTypes = new TypeDescriptor[paramCount];
for (int i = 0; i < paramCount; i++) {
this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i));
}
}
@Override
public T mapRow(ResultSet rs, int rowNumber) throws SQLException {
Object[] args = new Object[this.constructorParameterNames.length];
Set<Integer> usedIndex = new HashSet<>();
for (int i = 0; i < args.length; i++) {
String name = this.constructorParameterNames[i];
int index;
try {
// Try direct name match first
index = rs.findColumn(name);
}
catch (SQLException ex) {
// Try underscored name match instead
index = rs.findColumn(JdbcUtils.convertPropertyNameToUnderscoreName(name));
}
TypeDescriptor td = this.constructorParameterTypes[i];
Object value = JdbcUtils.getResultSetValue(rs, index, td.getType());
usedIndex.add(index);
args[i] = this.conversionService.convert(value, td);
}
T mappedObject = BeanUtils.instantiateClass(this.mappedConstructor, args);
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
for (int index = 1; index <= columnCount; index++) {
if (!usedIndex.contains(index)) {
Object desc = getDescriptor(JdbcUtils.lookupColumnName(rsmd, index));
if (desc instanceof MethodParameter mp) {
Method method = mp.getMethod();
if (method != null) {
Object value = JdbcUtils.getResultSetValue(rs, index, mp.getParameterType());
value = this.conversionService.convert(value, new TypeDescriptor(mp));
ReflectionUtils.makeAccessible(method);
ReflectionUtils.invokeMethod(method, mappedObject, value);
}
}
else if (desc instanceof Field field) {
Object value = JdbcUtils.getResultSetValue(rs, index, field.getType());
value = this.conversionService.convert(value, new TypeDescriptor(field));
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, mappedObject, value);
}
}
}
return mappedObject;
}
private Object getDescriptor(String column) {
return this.propertyDescriptors.computeIfAbsent(column, name -> {
// Try direct match first
PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(this.mappedClass, name);
if (pd != null && pd.getWriteMethod() != null) {
return BeanUtils.getWriteMethodParameter(pd);
}
Field field = ReflectionUtils.findField(this.mappedClass, name);
if (field != null) {
return field;
}
// Try de-underscored match instead
String adaptedName = JdbcUtils.convertUnderscoreNameToPropertyName(name);
if (!adaptedName.equals(name)) {
pd = BeanUtils.getPropertyDescriptor(this.mappedClass, adaptedName);
if (pd != null && pd.getWriteMethod() != null) {
return BeanUtils.getWriteMethodParameter(pd);
}
field = ReflectionUtils.findField(this.mappedClass, adaptedName);
if (field != null) {
return field;
}
}
// Fallback: case-insensitive match
PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(this.mappedClass);
for (PropertyDescriptor candidate : pds) {
if (name.equalsIgnoreCase(candidate.getName())) {
return BeanUtils.getWriteMethodParameter(candidate);
}
}
field = ReflectionUtils.findFieldIgnoreCase(this.mappedClass, name);
if (field != null) {
return field;
}
return NO_DESCRIPTOR;
});
}
}

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -53,6 +53,7 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> { @@ -53,6 +53,7 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> {
@Nullable
private ConversionService conversionService = DefaultConversionService.getSharedInstance();
/**
* Create a new {@code SingleColumnRowMapper} for bean-style configuration.
* @see #setRequiredType
@ -65,7 +66,9 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> { @@ -65,7 +66,9 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> {
* @param requiredType the type that each result object is expected to match
*/
public SingleColumnRowMapper(Class<T> requiredType) {
setRequiredType(requiredType);
if (requiredType != Object.class) {
setRequiredType(requiredType);
}
}

25
spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java

@ -18,8 +18,8 @@ package org.springframework.jdbc.core.namedparam; @@ -18,8 +18,8 @@ package org.springframework.jdbc.core.namedparam;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.core.StatementCreatorUtils;
@ -44,12 +44,16 @@ import org.springframework.util.ReflectionUtils; @@ -44,12 +44,16 @@ import org.springframework.util.ReflectionUtils;
* @since 6.1
* @see NamedParameterJdbcTemplate
* @see BeanPropertySqlParameterSource
* @see org.springframework.jdbc.core.simple.JdbcClient.StatementSpec#paramSource(Object)
* @see org.springframework.jdbc.core.SimplePropertyRowMapper
*/
public class SimplePropertySqlParameterSource extends AbstractSqlParameterSource {
private static final Object NO_DESCRIPTOR = new Object();
private final Object paramObject;
private final Map<String, Object> descriptorMap = new HashMap<>();
private final Map<String, Object> propertyDescriptors = new ConcurrentHashMap<>();
/**
@ -64,7 +68,7 @@ public class SimplePropertySqlParameterSource extends AbstractSqlParameterSource @@ -64,7 +68,7 @@ public class SimplePropertySqlParameterSource extends AbstractSqlParameterSource
@Override
public boolean hasValue(String paramName) {
return (getDescriptor(paramName) != null);
return (getDescriptor(paramName) != NO_DESCRIPTOR);
}
@Override
@ -103,14 +107,17 @@ public class SimplePropertySqlParameterSource extends AbstractSqlParameterSource @@ -103,14 +107,17 @@ public class SimplePropertySqlParameterSource extends AbstractSqlParameterSource
return TYPE_UNKNOWN;
}
@Nullable
private Object getDescriptor(String paramName) {
return this.descriptorMap.computeIfAbsent(paramName, name -> {
Object pd = BeanUtils.getPropertyDescriptor(this.paramObject.getClass(), name);
if (pd == null) {
pd = ReflectionUtils.findField(this.paramObject.getClass(), name);
return this.propertyDescriptors.computeIfAbsent(paramName, name -> {
PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(this.paramObject.getClass(), name);
if (pd != null && pd.getReadMethod() != null) {
return pd;
}
Field field = ReflectionUtils.findField(this.paramObject.getClass(), name);
if (field != null) {
return field;
}
return pd;
return NO_DESCRIPTOR;
});
}

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

@ -19,15 +19,19 @@ package org.springframework.jdbc.core.simple; @@ -19,15 +19,19 @@ package org.springframework.jdbc.core.simple;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SimplePropertyRowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
import org.springframework.jdbc.core.SqlParameterValue;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@ -54,6 +58,8 @@ final class DefaultJdbcClient implements JdbcClient { @@ -54,6 +58,8 @@ final class DefaultJdbcClient implements JdbcClient {
private final NamedParameterJdbcOperations namedParamOps;
private final Map<Class<?>, RowMapper<?>> rowMapperCache = new ConcurrentHashMap<>();
public DefaultJdbcClient(DataSource dataSource) {
this.classicOps = new JdbcTemplate(dataSource);
@ -169,6 +175,15 @@ final class DefaultJdbcClient implements JdbcClient { @@ -169,6 +175,15 @@ final class DefaultJdbcClient implements JdbcClient {
new IndexedParamResultQuerySpec());
}
@SuppressWarnings("unchecked")
@Override
public <T> MappedQuerySpec<T> query(Class<T> mappedClass) {
RowMapper<?> rowMapper = rowMapperCache.computeIfAbsent(mappedClass, key ->
BeanUtils.isSimpleProperty(mappedClass) ? new SingleColumnRowMapper<>(mappedClass) :
new SimplePropertyRowMapper<>(mappedClass));
return query((RowMapper<T>) rowMapper);
}
@Override
public <T> MappedQuerySpec<T> query(RowMapper<T> rowMapper) {
return (useNamedParams() ?
@ -239,9 +254,10 @@ final class DefaultJdbcClient implements JdbcClient { @@ -239,9 +254,10 @@ final class DefaultJdbcClient implements JdbcClient {
return classicOps.queryForMap(sql, indexedParams.toArray());
}
@SuppressWarnings("unchecked")
@Override
public <T> List<T> singleColumn(Class<T> requiredType) {
return classicOps.queryForList(sql, requiredType, indexedParams.toArray());
public <T> List<T> singleColumn() {
return (List<T>) classicOps.queryForList(sql, Object.class, indexedParams.toArray());
}
}
@ -263,9 +279,10 @@ final class DefaultJdbcClient implements JdbcClient { @@ -263,9 +279,10 @@ final class DefaultJdbcClient implements JdbcClient {
return namedParamOps.queryForMap(sql, namedParamSource);
}
@SuppressWarnings("unchecked")
@Override
public <T> List<T> singleColumn(Class<T> requiredType) {
return namedParamOps.queryForList(sql, namedParamSource, requiredType);
public <T> List<T> singleColumn() {
return (List<T>) namedParamOps.queryForList(sql, namedParamSource, Object.class);
}
}

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

@ -193,8 +193,10 @@ public interface JdbcClient { @@ -193,8 +193,10 @@ public interface JdbcClient {
* based on its JavaBean properties, record components or raw fields.
* A Map instance can be provided as a complete parameter source as well.
* @param namedParamObject a custom parameter object (e.g. a JavaBean or
* record class) with named properties serving as statement parameters
* record class or field holder) with named properties serving as
* statement parameters
* @return this statement specification (for chaining)
* @see #paramSource(SqlParameterSource)
* @see org.springframework.jdbc.core.namedparam.MapSqlParameterSource
* @see org.springframework.jdbc.core.namedparam.SimplePropertySqlParameterSource
*/
@ -218,6 +220,19 @@ public interface JdbcClient { @@ -218,6 +220,19 @@ public interface JdbcClient {
*/
ResultQuerySpec query();
/**
* Proceed towards execution of a mapped query, with several options
* available in the returned query specification.
* @param mappedClass the target class to apply a RowMapper for
* (either a simple value type for a single column mapping or a
* JavaBean / record class / field holder for a multi-column mapping)
* @return the mapped query specification
* @see #query(RowMapper)
* @see org.springframework.jdbc.core.SingleColumnRowMapper
* @see org.springframework.jdbc.core.SimplePropertyRowMapper
*/
<T> MappedQuerySpec<T> query(Class<T> mappedClass);
/**
* Proceed towards execution of a mapped query, with several options
* available in the returned query specification.
@ -296,7 +311,7 @@ public interface JdbcClient { @@ -296,7 +311,7 @@ public interface JdbcClient {
* @return a (potentially empty) list of rows, with each
* row represented as a column value of the given type
*/
<T> List<T> singleColumn(Class<T> requiredType);
<T> List<T> singleColumn();
/**
* Retrieve a single value result.
@ -304,8 +319,8 @@ public interface JdbcClient { @@ -304,8 +319,8 @@ public interface JdbcClient {
* column value of the given type
* @see DataAccessUtils#requiredSingleResult(Collection)
*/
default <T> T singleValue(Class<T> requiredType) {
return DataAccessUtils.requiredSingleResult(singleColumn(requiredType));
default <T> T singleValue() {
return DataAccessUtils.requiredSingleResult(singleColumn());
}
}

70
spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -510,34 +510,64 @@ public abstract class JdbcUtils { @@ -510,34 +510,64 @@ public abstract class JdbcUtils {
}
/**
* Convert a column name with underscores to the corresponding property name using "camel case".
* Convert a property name using "camelCase" to a corresponding column name with underscores.
* A name like "customerNumber" would match a "customer_number" column name.
* @param name the property name to be converted
* @return the column name using underscores
* @since 6.1
* @see #convertUnderscoreNameToPropertyName
*/
public static String convertPropertyNameToUnderscoreName(@Nullable String name) {
if (!StringUtils.hasLength(name)) {
return "";
}
StringBuilder result = new StringBuilder();
result.append(Character.toLowerCase(name.charAt(0)));
for (int i = 1; i < name.length(); i++) {
char c = name.charAt(i);
if (Character.isUpperCase(c)) {
result.append('_').append(Character.toLowerCase(c));
}
else {
result.append(c);
}
}
return result.toString();
}
/**
* Convert a column name with underscores to the corresponding property name using "camelCase".
* A name like "customer_number" would match a "customerNumber" property name.
* @param name the column name to be converted
* @return the name using "camel case"
* @param name the potentially underscores-based column name to be converted
* @return the name using "camelCase"
* @see #convertPropertyNameToUnderscoreName
*/
public static String convertUnderscoreNameToPropertyName(@Nullable String name) {
if (!StringUtils.hasLength(name)) {
return "";
}
StringBuilder result = new StringBuilder();
boolean nextIsUpper = false;
if (name != null && name.length() > 0) {
if (name.length() > 1 && name.charAt(1) == '_') {
result.append(Character.toUpperCase(name.charAt(0)));
if (name.length() > 1 && name.charAt(1) == '_') {
result.append(Character.toUpperCase(name.charAt(0)));
}
else {
result.append(Character.toLowerCase(name.charAt(0)));
}
for (int i = 1; i < name.length(); i++) {
char c = name.charAt(i);
if (c == '_') {
nextIsUpper = true;
}
else {
result.append(Character.toLowerCase(name.charAt(0)));
}
for (int i = 1; i < name.length(); i++) {
char c = name.charAt(i);
if (c == '_') {
nextIsUpper = true;
if (nextIsUpper) {
result.append(Character.toUpperCase(c));
nextIsUpper = false;
}
else {
if (nextIsUpper) {
result.append(Character.toUpperCase(c));
nextIsUpper = false;
}
else {
result.append(Character.toLowerCase(c));
}
result.append(Character.toLowerCase(c));
}
}
}

1
spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java

@ -167,6 +167,7 @@ public abstract class AbstractRowMapperTests { @@ -167,6 +167,7 @@ public abstract class AbstractRowMapperTests {
}
else {
given(resultSet.findColumn("birthdate")).willThrow(new SQLException());
given(resultSet.findColumn("birthDate")).willThrow(new SQLException());
given(resultSet.findColumn("birth_date")).willReturn(3);
}
given(resultSet.findColumn("balance")).willReturn(4);

2
spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java

@ -95,7 +95,7 @@ class DataClassRowMapperTests extends AbstractRowMapperTests { @@ -95,7 +95,7 @@ class DataClassRowMapperTests extends AbstractRowMapperTests {
}
static record RecordPerson(String name, long age, Date birth_date, BigDecimal balance) {
record RecordPerson(String name, long age, Date birth_date, BigDecimal balance) {
}
}

165
spring-jdbc/src/test/java/org/springframework/jdbc/core/SimplePropertyRowMapperTests.java

@ -0,0 +1,165 @@ @@ -0,0 +1,165 @@
/*
* Copyright 2002-2023 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.jdbc.core;
import java.math.BigDecimal;
import java.util.Date;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.test.ConcretePerson;
import org.springframework.jdbc.core.test.ConstructorPerson;
import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics;
import org.springframework.jdbc.core.test.ConstructorPersonWithSetters;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SimplePropertyRowMapper}.
*
* @author Juergen Hoeller
* @since 6.1
*/
class SimplePropertyRowMapperTests extends AbstractRowMapperTests {
@Test
void staticQueryWithDataClass() throws Exception {
Mock mock = new Mock();
ConstructorPerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new SimplePropertyRowMapper<>(ConstructorPerson.class));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void staticQueryWithDataClassAndGenerics() throws Exception {
Mock mock = new Mock();
ConstructorPersonWithGenerics person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new SimplePropertyRowMapper<>(ConstructorPersonWithGenerics.class));
assertThat(person.name()).isEqualTo("Bubba");
assertThat(person.age()).isEqualTo(22L);
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance()).containsExactly(new BigDecimal("1234.56"));
mock.verifyClosed();
}
@Test
void staticQueryWithDataClassAndSetters() throws Exception {
Mock mock = new Mock(MockType.FOUR);
ConstructorPersonWithSetters person = mock.getJdbcTemplate().queryForObject(
"select name, age, birthdate, balance from people",
new SimplePropertyRowMapper<>(ConstructorPersonWithSetters.class));
assertThat(person.name()).isEqualTo("BUBBA");
assertThat(person.age()).isEqualTo(22L);
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56"));
mock.verifyClosed();
}
@Test
void staticQueryWithPlainSetters() throws Exception {
Mock mock = new Mock();
ConcretePerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new SimplePropertyRowMapper<>(ConcretePerson.class));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void staticQueryWithDataRecord() throws Exception {
Mock mock = new Mock();
RecordPerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new SimplePropertyRowMapper<>(RecordPerson.class));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void staticQueryWithDataFields() throws Exception {
Mock mock = new Mock();
FieldPerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new SimplePropertyRowMapper<>(FieldPerson.class));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void staticQueryWithIncompleteDataFields() throws Exception {
Mock mock = new Mock();
IncompleteFieldPerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new SimplePropertyRowMapper<>(IncompleteFieldPerson.class));
verifyPerson(person);
mock.verifyClosed();
}
protected void verifyPerson(RecordPerson person) {
assertThat(person.name()).isEqualTo("Bubba");
assertThat(person.age()).isEqualTo(22L);
assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56"));
verifyPersonViaBeanWrapper(person);
}
protected void verifyPerson(FieldPerson person) {
assertThat(person.name).isEqualTo("Bubba");
assertThat(person.age).isEqualTo(22L);
assertThat(person.birth_date).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance).isEqualTo(new BigDecimal("1234.56"));
}
protected void verifyPerson(IncompleteFieldPerson person) {
assertThat(person.name).isEqualTo("Bubba");
assertThat(person.age).isEqualTo(22L);
assertThat(person.balance).isEqualTo(new BigDecimal("1234.56"));
}
record RecordPerson(String name, long age, Date birth_date, BigDecimal balance) {
}
static class FieldPerson {
String name;
long age;
Date birth_date;
BigDecimal balance;
}
static class IncompleteFieldPerson {
String name;
long age;
BigDecimal balance;
}
}

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

@ -108,7 +108,7 @@ public class JdbcClientQueryTests { @@ -108,7 +108,7 @@ public class JdbcClientQueryTests {
}
@Test
public void testQueryForListWithIndexedParamAndSingleRowAndColumn() throws Exception {
public void testQueryForListWithIndexedParamAndSingleRow() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getObject(1)).willReturn(11);
@ -123,14 +123,14 @@ public class JdbcClientQueryTests { @@ -123,14 +123,14 @@ public class JdbcClientQueryTests {
}
@Test
public void testQueryForListWithIndexedParamAndIntegerElementAndSingleRowAndColumn() throws Exception {
public void testQueryForListWithIndexedParamAndSingleColumn() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(11);
given(resultSet.getObject(1)).willReturn(11);
List<Integer> li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < ?")
.param(1, 3)
.query().singleColumn(Integer.class);
.query().singleColumn();
assertThat(li.size()).as("All rows returned").isEqualTo(1);
assertThat(li.get(0)).as("First row is Integer").isEqualTo(11);
@ -139,7 +139,7 @@ public class JdbcClientQueryTests { @@ -139,7 +139,7 @@ public class JdbcClientQueryTests {
}
@Test
public void testQueryForMapWithIndexedParamAndSingleRowAndColumn() throws Exception {
public void testQueryForMapWithIndexedParamAndSingleRow() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getObject(1)).willReturn(11);
@ -185,11 +185,11 @@ public class JdbcClientQueryTests { @@ -185,11 +185,11 @@ public class JdbcClientQueryTests {
public void testQueryForObjectWithIndexedParamAndInteger() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(22);
given(resultSet.getObject(1)).willReturn(22);
Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = ?")
.param(1, 3)
.query().singleValue(Integer.class);
.query().singleValue();
assertThat(value).isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
@ -200,11 +200,11 @@ public class JdbcClientQueryTests { @@ -200,11 +200,11 @@ public class JdbcClientQueryTests {
public void testQueryForIntWithIndexedParam() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(22);
given(resultSet.getObject(1)).willReturn(22);
int i = client.sql("SELECT AGE FROM CUSTMR WHERE ID = ?")
.param(1, 3)
.query().singleValue(Integer.class);
.query().singleValue();
assertThat(i).as("Return of an int").isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
@ -246,7 +246,7 @@ public class JdbcClientQueryTests { @@ -246,7 +246,7 @@ public class JdbcClientQueryTests {
}
@Test
public void testQueryForListWithNamedParamAndSingleRowAndColumn() throws Exception {
public void testQueryForListWithNamedParamAndSingleRow() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getObject(1)).willReturn(11);
@ -262,14 +262,14 @@ public class JdbcClientQueryTests { @@ -262,14 +262,14 @@ public class JdbcClientQueryTests {
}
@Test
public void testQueryForListWithNamedParamAndIntegerElementAndSingleRowAndColumn() throws Exception {
public void testQueryForListWithNamedParamAndSingleColumn() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(11);
given(resultSet.getObject(1)).willReturn(11);
List<Integer> li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < :id")
.param("id", 3)
.query().singleColumn(Integer.class);
.query().singleColumn();
assertThat(li.size()).as("All rows returned").isEqualTo(1);
assertThat(li.get(0)).as("First row is Integer").isEqualTo(11);
@ -278,7 +278,7 @@ public class JdbcClientQueryTests { @@ -278,7 +278,7 @@ public class JdbcClientQueryTests {
}
@Test
public void testQueryForMapWithNamedParamAndSingleRowAndColumn() throws Exception {
public void testQueryForMapWithNamedParamAndSingleRow() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getObject(1)).willReturn(11);
@ -308,14 +308,15 @@ public class JdbcClientQueryTests { @@ -308,14 +308,15 @@ public class JdbcClientQueryTests {
}
@Test
public void testQueryForObjectWithNamedParamAndInteger() throws Exception {
public void testQueryForObjectWithNamedParamAndMappedSimpleValue() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(22);
Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.param("id", 3)
.query().singleValue(Integer.class);
.query(Integer.class)
.single();
assertThat(value).isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
@ -323,14 +324,62 @@ public class JdbcClientQueryTests { @@ -323,14 +324,62 @@ public class JdbcClientQueryTests {
}
@Test
public void testQueryForObjectWithNamedParamAndList() throws Exception {
public void testQueryForObjectWithNamedParamAndMappedRecord() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.findColumn("age")).willReturn(1);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(22);
AgeRecord value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.param("id", 3)
.query(AgeRecord.class)
.single();
assertThat(value.age()).isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
verify(preparedStatement).setObject(1, 3);
}
@Test
public void testQueryForObjectWithNamedParamAndMappedFieldHolder() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(22);
AgeFieldHolder value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.param("id", 3)
.query(AgeFieldHolder.class)
.single();
assertThat(value.age).isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
verify(preparedStatement).setObject(1, 3);
}
@Test
public void testQueryForObjectWithNamedParamAndInteger() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getObject(1)).willReturn(22);
Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.param("id", 3)
.query().singleValue();
assertThat(value).isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
verify(preparedStatement).setObject(1, 3);
}
@Test
public void testQueryForObjectWithNamedParamAndList() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getObject(1)).willReturn(22);
Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID IN (:ids)")
.param("ids", Arrays.asList(3, 4))
.query().singleValue(Integer.class);
.query().singleValue();
assertThat(value).isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)");
@ -341,14 +390,14 @@ public class JdbcClientQueryTests { @@ -341,14 +390,14 @@ public class JdbcClientQueryTests {
public void testQueryForObjectWithNamedParamAndListOfExpressionLists() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(22);
given(resultSet.getObject(1)).willReturn(22);
List<Object[]> l1 = new ArrayList<>();
l1.add(new Object[] {3, "Rod"});
l1.add(new Object[] {4, "Juergen"});
Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)")
.param("multiExpressionList", l1)
.query().singleValue(Integer.class);
.query().singleValue();
assertThat(value).isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))");
@ -359,11 +408,11 @@ public class JdbcClientQueryTests { @@ -359,11 +408,11 @@ public class JdbcClientQueryTests {
public void testQueryForIntWithNamedParam() throws Exception {
given(resultSet.getMetaData()).willReturn(resultSetMetaData);
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(22);
given(resultSet.getObject(1)).willReturn(22);
int i = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.param("id", 3)
.query().singleValue(Integer.class);
.query().singleValue();
assertThat(i).as("Return of an int").isEqualTo(22);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
@ -378,7 +427,7 @@ public class JdbcClientQueryTests { @@ -378,7 +427,7 @@ public class JdbcClientQueryTests {
long l = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.paramSource(new ParameterBean(3))
.query().singleValue(Long.class);
.query(Long.class).single();
assertThat(l).as("Return of a long").isEqualTo(87);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
@ -393,7 +442,7 @@ public class JdbcClientQueryTests { @@ -393,7 +442,7 @@ public class JdbcClientQueryTests {
long l = client.sql("SELECT AGE FROM CUSTMR WHERE ID IN (:ids)")
.paramSource(new ParameterCollectionBean(3, 5))
.query().singleValue(Long.class);
.query(Long.class).single();
assertThat(l).as("Return of a long").isEqualTo(87);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)");
@ -409,7 +458,7 @@ public class JdbcClientQueryTests { @@ -409,7 +458,7 @@ public class JdbcClientQueryTests {
long l = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.paramSource(new ParameterRecord(3))
.query().singleValue(Long.class);
.query(Long.class).single();
assertThat(l).as("Return of a long").isEqualTo(87);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
@ -424,7 +473,7 @@ public class JdbcClientQueryTests { @@ -424,7 +473,7 @@ public class JdbcClientQueryTests {
long l = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
.paramSource(new ParameterFieldHolder(3))
.query().singleValue(Long.class);
.query(Long.class).single();
assertThat(l).as("Return of a long").isEqualTo(87);
verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?");
@ -473,4 +522,14 @@ public class JdbcClientQueryTests { @@ -473,4 +522,14 @@ public class JdbcClientQueryTests {
public int id;
}
record AgeRecord(int age) {
}
static class AgeFieldHolder {
public int age;
}
}

10
spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -29,15 +29,15 @@ public class ConstructorPersonWithGenerics { @@ -29,15 +29,15 @@ public class ConstructorPersonWithGenerics {
private final long age;
private final Date birthDate;
private final Date bd;
private final List<BigDecimal> balance;
public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List<BigDecimal> balance) {
public ConstructorPersonWithGenerics(String name, long age, Date birthDate, List<BigDecimal> balance) {
this.name = name;
this.age = age;
this.birthDate = birth_date;
this.bd = birthDate;
this.balance = balance;
}
@ -51,7 +51,7 @@ public class ConstructorPersonWithGenerics { @@ -51,7 +51,7 @@ public class ConstructorPersonWithGenerics {
}
public Date birthDate() {
return this.birthDate;
return this.bd;
}
public List<BigDecimal> balance() {

11
spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -33,20 +33,19 @@ public class ConstructorPersonWithSetters { @@ -33,20 +33,19 @@ public class ConstructorPersonWithSetters {
private BigDecimal balance;
public ConstructorPersonWithSetters(String name, long age, Date birthDate, BigDecimal balance) {
public ConstructorPersonWithSetters(String name, long age, BigDecimal balance) {
this.name = name.toUpperCase();
this.age = age;
this.birthDate = birthDate;
this.balance = balance;
}
public void setName(String name) {
this.name = name;
throw new UnsupportedOperationException();
}
public void setAge(long age) {
this.age = age;
throw new UnsupportedOperationException();
}
public void setBirthDate(Date birthDate) {
@ -54,7 +53,7 @@ public class ConstructorPersonWithSetters { @@ -54,7 +53,7 @@ public class ConstructorPersonWithSetters {
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
throw new UnsupportedOperationException();
}
public String name() {

Loading…
Cancel
Save