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 5b8192c3175..417c257efc3 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 @@ -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. * - *

For {@code NULL} values read from the database, an attempt will be made to + *

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. * *

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 the result type * @see DataClassRowMapper @@ -96,7 +96,11 @@ public class BeanPropertyRowMapper implements RowMapper { /** 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 implements RowMapper { } /** - * Set whether we're defaulting Java primitives in the case of mapping a null value - * from corresponding database fields. - *

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. + *

Default is {@code false}, throwing an exception when nulls are mapped + * to Java primitives. + *

If this flag is set to {@code true} and you use an ignored + * 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 implements RowMapper { 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 { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java index 5ef1f57f891..4d1d53e6367 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.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. @@ -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 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 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 diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java index e1d9149d512..b9be634d949 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java @@ -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;