diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java index 0fdbb75d476..02d74cf3ff9 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -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}. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java index 2749965df2b..3e7572c85c5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java @@ -61,7 +61,7 @@ import org.springframework.util.StringUtils; * long, Long, float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc. * *

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; * @since 2.5 * @param the result type * @see DataClassRowMapper + * @see SimplePropertyRowMapper */ public class BeanPropertyRowMapper implements RowMapper { @@ -278,6 +279,7 @@ public class BeanPropertyRowMapper implements RowMapper { * @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 implements RowMapper { * @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); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index eba4e83aca1..c75eae3b118 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -57,6 +57,7 @@ import org.springframework.util.Assert; * @author Sam Brannen * @since 5.3 * @param the result type + * @see SimplePropertyRowMapper */ public class DataClassRowMapper extends BeanPropertyRowMapper { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.java new file mode 100644 index 00000000000..e15c8e21e3a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.java @@ -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 + * data class 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 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 implements RowMapper { + + private static final Object NO_DESCRIPTOR = new Object(); + + private final Class mappedClass; + + private final ConversionService conversionService; + + private final Constructor mappedConstructor; + + private final String[] constructorParameterNames; + + private final TypeDescriptor[] constructorParameterTypes; + + private final Map propertyDescriptors = new ConcurrentHashMap<>(); + + + /** + * Create a new {@code SimplePropertyRowMapper}. + * @param mappedClass the class that each row should be mapped to + */ + public SimplePropertyRowMapper(Class 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 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 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; + }); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java index b6669a86709..43dffc066eb 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java @@ -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 implements RowMapper { @Nullable private ConversionService conversionService = DefaultConversionService.getSharedInstance(); + /** * Create a new {@code SingleColumnRowMapper} for bean-style configuration. * @see #setRequiredType @@ -65,7 +66,9 @@ public class SingleColumnRowMapper implements RowMapper { * @param requiredType the type that each result object is expected to match */ public SingleColumnRowMapper(Class requiredType) { - setRequiredType(requiredType); + if (requiredType != Object.class) { + setRequiredType(requiredType); + } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java index f5c873ef50d..60583679a72 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java @@ -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; * @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 descriptorMap = new HashMap<>(); + private final Map propertyDescriptors = new ConcurrentHashMap<>(); /** @@ -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 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; }); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java index 155de603531..8f5c41473fc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java @@ -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 { private final NamedParameterJdbcOperations namedParamOps; + private final Map, RowMapper> rowMapperCache = new ConcurrentHashMap<>(); + public DefaultJdbcClient(DataSource dataSource) { this.classicOps = new JdbcTemplate(dataSource); @@ -169,6 +175,15 @@ final class DefaultJdbcClient implements JdbcClient { new IndexedParamResultQuerySpec()); } + @SuppressWarnings("unchecked") + @Override + public MappedQuerySpec query(Class mappedClass) { + RowMapper rowMapper = rowMapperCache.computeIfAbsent(mappedClass, key -> + BeanUtils.isSimpleProperty(mappedClass) ? new SingleColumnRowMapper<>(mappedClass) : + new SimplePropertyRowMapper<>(mappedClass)); + return query((RowMapper) rowMapper); + } + @Override public MappedQuerySpec query(RowMapper rowMapper) { return (useNamedParams() ? @@ -239,9 +254,10 @@ final class DefaultJdbcClient implements JdbcClient { return classicOps.queryForMap(sql, indexedParams.toArray()); } + @SuppressWarnings("unchecked") @Override - public List singleColumn(Class requiredType) { - return classicOps.queryForList(sql, requiredType, indexedParams.toArray()); + public List singleColumn() { + return (List) classicOps.queryForList(sql, Object.class, indexedParams.toArray()); } } @@ -263,9 +279,10 @@ final class DefaultJdbcClient implements JdbcClient { return namedParamOps.queryForMap(sql, namedParamSource); } + @SuppressWarnings("unchecked") @Override - public List singleColumn(Class requiredType) { - return namedParamOps.queryForList(sql, namedParamSource, requiredType); + public List singleColumn() { + return (List) namedParamOps.queryForList(sql, namedParamSource, Object.class); } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java index fe122fc9276..c3253717e08 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java @@ -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 { */ 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 + */ + MappedQuerySpec query(Class mappedClass); + /** * Proceed towards execution of a mapped query, with several options * available in the returned query specification. @@ -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 */ - List singleColumn(Class requiredType); + List singleColumn(); /** * Retrieve a single value result. @@ -304,8 +319,8 @@ public interface JdbcClient { * column value of the given type * @see DataAccessUtils#requiredSingleResult(Collection) */ - default T singleValue(Class requiredType) { - return DataAccessUtils.requiredSingleResult(singleColumn(requiredType)); + default T singleValue() { + return DataAccessUtils.requiredSingleResult(singleColumn()); } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java index fb42d833a4f..7825b39c13a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java @@ -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 { } /** - * 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)); } } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 1a43910e8da..42c35732be4 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index a796a14c67f..7531a1849bc 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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) { } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/SimplePropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/SimplePropertyRowMapperTests.java new file mode 100644 index 00000000000..bd75770dd35 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/SimplePropertyRowMapperTests.java @@ -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; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java index b0a14fc45bb..4d006aa34e3 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java @@ -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 { } @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 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 { } @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 { 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 { 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 { } @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 { } @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 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 { } @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 { } @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 { } @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 { 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 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 { 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 { 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 { 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 { 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 { 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 { public int id; } + + record AgeRecord(int age) { + } + + + static class AgeFieldHolder { + + public int age; + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java index 289197b5639..6edd3f9ef69 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -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 { private final long age; - private final Date birthDate; + private final Date bd; private final List balance; - public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + public ConstructorPersonWithGenerics(String name, long age, Date birthDate, List balance) { this.name = name; this.age = age; - this.birthDate = birth_date; + this.bd = birthDate; this.balance = balance; } @@ -51,7 +51,7 @@ public class ConstructorPersonWithGenerics { } public Date birthDate() { - return this.birthDate; + return this.bd; } public List balance() { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java index 0776b5cc48a..4cf527a007a 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java @@ -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 { 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 { } public void setBalance(BigDecimal balance) { - this.balance = balance; + throw new UnsupportedOperationException(); } public String name() {