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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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