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 397c21f678..446ef64c5e 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 @@ -225,16 +225,40 @@ public class BeanPropertyRowMapper implements RowMapper { for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) { if (pd.getWriteMethod() != null) { - this.mappedFields.put(lowerCaseName(pd.getName()), pd); - String underscoredName = underscoreName(pd.getName()); - if (!lowerCaseName(pd.getName()).equals(underscoredName)) { - this.mappedFields.put(underscoredName, pd); + String lowerCaseName = lowerCaseName(pd.getName()); + this.mappedFields.put(lowerCaseName, pd); + String underscoreName = underscoreName(pd.getName()); + if (!lowerCaseName.equals(underscoreName)) { + this.mappedFields.put(underscoreName, pd); } this.mappedProperties.add(pd.getName()); } } } + /** + * Remove the specified property from the mapped fields. + * @param propertyName the property name (as used by property descriptors) + * @since 5.3.9 + */ + protected void suppressProperty(String propertyName) { + if (this.mappedFields != null) { + this.mappedFields.remove(lowerCaseName(propertyName)); + this.mappedFields.remove(underscoreName(propertyName)); + } + } + + /** + * Convert the given name to lower case. + * By default, conversions will happen within the US locale. + * @param name the original name + * @return the converted name + * @since 4.2 + */ + protected String lowerCaseName(String name) { + return name.toLowerCase(Locale.US); + } + /** * Convert a name in camelCase to an underscored name in lower case. * Any upper case letters are converted to lower case with a preceding underscore. @@ -261,17 +285,6 @@ public class BeanPropertyRowMapper implements RowMapper { return result.toString(); } - /** - * Convert the given name to lower case. - * By default, conversions will happen within the US locale. - * @param name the original name - * @return the converted name - * @since 4.2 - */ - protected String lowerCaseName(String name) { - return name.toLowerCase(Locale.US); - } - /** * Extract the values for all columns in the current row. 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 6783441fce..7e09fcbe89 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 @@ -80,6 +80,9 @@ public class DataClassRowMapper extends BeanPropertyRowMapper { int paramCount = this.mappedConstructor.getParameterCount(); if (paramCount > 0) { this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); + for (String name : this.constructorParameterNames) { + suppressProperty(name); + } this.constructorParameterTypes = new TypeDescriptor[paramCount]; for (int i = 0; i < paramCount; i++) { this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i)); 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 473cb6f14c..48b0f7f031 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 @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; 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; @@ -62,4 +63,20 @@ public class DataClassRowMapperTests extends AbstractRowMapperTests { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndSetters() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithSetters.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithSetters person = result.get(0); + assertThat(person.name()).isEqualTo("BUBBA"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56")); + + mock.verifyClosed(); + } + } 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 new file mode 100644 index 0000000000..ef1feb9a32 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2021 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.test; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithSetters { + + private String name; + + private long age; + + private Date birth_date; + + private BigDecimal balance; + + + public ConstructorPersonWithSetters(String name, long age, Date birth_date, BigDecimal balance) { + this.name = name.toUpperCase(); + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public void setName(String name) { + this.name = name; + } + + public void setAge(long age) { + this.age = age; + } + + public void setBirth_date(Date birth_date) { + this.birth_date = birth_date; + } + + public void setBalance(BigDecimal balance) { + this.balance = balance; + } + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public BigDecimal balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/KotlinDataClassRowMapperTests.kt b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/KotlinDataClassRowMapperTests.kt new file mode 100644 index 0000000000..53738d0a21 --- /dev/null +++ b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/KotlinDataClassRowMapperTests.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2021 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 org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.jdbc.core.test.ConstructorPerson +import java.math.BigDecimal +import java.util.* + +class KotlinDataClassRowMapperTests : AbstractRowMapperTests() { + + @Test + fun testStaticQueryWithDataClass() { + val mock = Mock() + val result = mock.jdbcTemplate.query( + "select name, age, birth_date, balance from people", + DataClassRowMapper(ConstructorPerson::class.java) + ) + Assertions.assertThat(result.size).isEqualTo(1) + verifyPerson(result[0]) + mock.verifyClosed() + } + + @Test + fun testInitPropertiesAreNotOverridden() { + val mock = Mock() + val result = mock.jdbcTemplate.query( + "select name, age, birth_date, balance from people", + DataClassRowMapper(KotlinPerson::class.java) + ) + Assertions.assertThat(result.size).isEqualTo(1) + Assertions.assertThat(result[0].name).isEqualTo("Bubba appended by init") + } + + + data class KotlinPerson(var name: String, val age: Long, val birth_date: Date, val balance: BigDecimal) { + init { + name += " appended by init" + } + } + +}