Clarify semantics of primitivesDefaultedForNullValue in BeanPropertyRowMapper

Closes gh-29923
This commit is contained in:
Sam Brannen 2023-02-03 17:37:23 +01:00
parent adbba712d6
commit 159a3e71f2
3 changed files with 50 additions and 21 deletions

View File

@ -65,14 +65,13 @@ import org.springframework.util.StringUtils;
* {@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>For {@code NULL} values read from the database, an attempt will be made to
* <p>For a {@code NULL} value read from the database, an attempt will be made to
* call the corresponding setter method with {@code null}, but in the case of
* Java primitives, this will result in a {@link TypeMismatchException}. This class
* can be configured (via the {@link #setPrimitivesDefaultedForNullValue(boolean)
* primitivesDefaultedForNullValue} property) to catch this exception and use the
* primitive's default value. Be aware that if you use the values from the mapped
* bean to update the database, the primitive value in the database will be
* changed from {@code NULL} to the primitive's default value.
* Java primitives this will result in a {@link TypeMismatchException} by default.
* To ignore {@code NULL} database values for all primitive properties in the
* target class, set the {@code primitivesDefaultedForNullValue} flag to
* {@code true}. See {@link #setPrimitivesDefaultedForNullValue(boolean)} for
* details.
*
* <p>Please note that this class is designed to provide convenience rather than
* high performance. For best performance, consider using a custom {@code RowMapper}
@ -80,6 +79,7 @@ import org.springframework.util.StringUtils;
*
* @author Thomas Risberg
* @author Juergen Hoeller
* @author Sam Brannen
* @since 2.5
* @param <T> the result type
* @see DataClassRowMapper
@ -96,7 +96,11 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
/** Whether we're strictly validating. */
private boolean checkFullyPopulated = false;
/** Whether we're defaulting primitives when mapping a null value. */
/**
* Whether {@code NULL} database values should be ignored for primitive
* properties in the target class.
* @see #setPrimitivesDefaultedForNullValue(boolean)
*/
private boolean primitivesDefaultedForNullValue = false;
/** ConversionService for binding JDBC values to bean properties. */
@ -182,17 +186,26 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
}
/**
* Set whether we're defaulting Java primitives in the case of mapping a null value
* from corresponding database fields.
* <p>Default is {@code false}, throwing an exception when nulls are mapped to Java primitives.
* Set whether a {@code NULL} database field value should be ignored when
* mapping to a corresponding primitive property in the target class.
* <p>Default is {@code false}, throwing an exception when nulls are mapped
* to Java primitives.
* <p>If this flag is set to {@code true} and you use an <em>ignored</em>
* primitive property value from the mapped bean to update the database, the
* value in the database will be changed from {@code NULL} to the current value
* of that primitive property. That value may be the field's initial value
* (potentially Java's default value for the respective primitive type), or
* it may be some other value set for the property in the default constructor
* (or initialization block) or as a side effect of setting some other property
* in the mapped bean.
*/
public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
}
/**
* Return whether we're defaulting Java primitives in the case of mapping a null value
* from corresponding database fields.
* Get the value of the {@code primitivesDefaultedForNullValue} flag.
* @see #setPrimitivesDefaultedForNullValue(boolean)
*/
public boolean isPrimitivesDefaultedForNullValue() {
return this.primitivesDefaultedForNullValue;
@ -328,11 +341,11 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
catch (TypeMismatchException ex) {
if (value == null && this.primitivesDefaultedForNullValue) {
if (logger.isDebugEnabled()) {
logger.debug("Intercepted TypeMismatchException for row " + rowNumber +
" and column '" + column + "' with null value when setting property '" +
pd.getName() + "' of type '" +
ClassUtils.getQualifiedName(pd.getPropertyType()) +
"' on object: " + mappedObject, ex);
String propertyType = ClassUtils.getQualifiedName(pd.getPropertyType());
logger.debug("""
Ignoring intercepted TypeMismatchException for row %d and column '%s' \
with null value when setting property '%s' of type '%s' on object: %s"
""".formatted(rowNumber, column, pd.getName(), propertyType, mappedObject), ex);
}
}
else {

View File

@ -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.
@ -44,6 +44,9 @@ import static org.assertj.core.api.Assertions.assertThatNoException;
*/
class BeanPropertyRowMapperTests extends AbstractRowMapperTests {
private static final String SELECT_NULL_AS_AGE = "select null as age from people";
@Test
@SuppressWarnings({"unchecked", "rawtypes"})
void overridingDifferentClassDefinedForMapping() {
@ -115,7 +118,17 @@ class BeanPropertyRowMapperTests extends AbstractRowMapperTests {
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
Mock mock = new Mock(MockType.TWO);
assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() ->
mock.getJdbcTemplate().query("select name, null as age, birth_date, balance from people", mapper));
mock.getJdbcTemplate().query(SELECT_NULL_AS_AGE, mapper));
}
@Test
void mappingNullValueWithPrimitivesDefaultedForNullValue() throws Exception {
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
mapper.setPrimitivesDefaultedForNullValue(true);
Mock mock = new Mock(MockType.TWO);
Person person = mock.getJdbcTemplate().queryForObject(SELECT_NULL_AS_AGE, mapper);
assertThat(person).extracting(Person::getAge).isEqualTo(42L);
mock.verifyClosed();
}
@Test

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@ -31,6 +31,9 @@ public class Person {
private BigDecimal balance;
public Person() {
this.age = 42; // custom "default" value for a primitive
}
public String getName() {
return name;