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