Browse Source
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-30931pull/31063/head
15 changed files with 617 additions and 95 deletions
@ -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; |
||||
}); |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue