From 93c56f19dfd39bd2f2cb6ffd94377a57c52a9ab6 Mon Sep 17 00:00:00 2001 From: Thomas Risberg Date: Wed, 25 Mar 2009 15:26:39 +0000 Subject: [PATCH] added a config property to control defaulting of primitive property when receiving null value from result (SPR-5588) --- .../jdbc/core/BeanPropertyRowMapper.java | 34 ++++++-- .../ParameterizedBeanPropertyRowMapper.java | 15 ++++ .../jdbc/core/AbstractRowMapperTests.java | 82 +++++++++++++++++++ .../jdbc/core/BeanPropertyRowMapperTests.java | 19 +++++ 4 files changed, 144 insertions(+), 6 deletions(-) diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java index 7b9d8a909e..5ae9462e97 100644 --- a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java @@ -56,7 +56,8 @@ import org.springframework.util.Assert; * try using column aliases in the SQL statement like "select fname as first_name from customer". * *

For 'null' values read from the databasem, we will attempt to call the setter, but in the case of - * primitives, this causes a TypeMismatchException. We will trap this exception and log a warning message. + * Java primitives, this causes a TypeMismatchException. This class can be configured (using the + * primitivesDefaultedForNullValue property) to trap this exception and use the primitives default value. * Be aware that if you use the values from the generated bean to update the database the primitive value * will have been set to the primitive's default value instead of null. * @@ -78,6 +79,9 @@ public class BeanPropertyRowMapper implements RowMapper { /** Whether we're strictly validating */ private boolean checkFullyPopulated = false; + /** Whether we're defaulting primitives when mapping a null value */ + private boolean primitivesDefaultedForNullValue = false; + /** Map of the fields we provide mapping for */ private Map mappedFields; @@ -199,6 +203,22 @@ public class BeanPropertyRowMapper implements RowMapper { return this.checkFullyPopulated; } + /** + * Set whether we're defaulting Java primitives in the case of mapping a null value from corresponding + * database fields. + *

Default is false, throwing an exception when nulls are mapped to Java primitives. + */ + public boolean isPrimitivesDefaultedForNullValue() { + return primitivesDefaultedForNullValue; + } + + /** + * Return whether we're defaulting Java primitives in the case of mapping a null value from corresponding + * database fields. + */ + public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) { + this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue; + } /** * Extract the values for all columns in the current row. @@ -229,11 +249,13 @@ public class BeanPropertyRowMapper implements RowMapper { bw.setPropertyValue(pd.getName(), value); } catch (TypeMismatchException e) { - logger.warn("Intercepted TypeMismatchException for row " + rowNumber + - " and column '" + column + "' with value " + value + - " when setting property '" + pd.getName() + "' of type " + pd.getPropertyType() + - " on object: " + mappedObject); - if (value != null) { + if (value == null && primitivesDefaultedForNullValue) { + logger.debug("Intercepted TypeMismatchException for row " + rowNumber + + " and column '" + column + "' with value " + value + + " when setting property '" + pd.getName() + "' of type " + pd.getPropertyType() + + " on object: " + mappedObject); + } + else { throw e; } } diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedBeanPropertyRowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedBeanPropertyRowMapper.java index 982ca8ea3e..037caf9482 100644 --- a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedBeanPropertyRowMapper.java +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedBeanPropertyRowMapper.java @@ -35,6 +35,10 @@ import org.springframework.jdbc.core.BeanPropertyRowMapper; * String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long, * float, Float, double, Double, BigDecimal, java.util.Date, etc. * + *

The mapper can be configured to use the primitives default value when mapping null values by + * passing in 'true' for the 'primitivesDefaultedForNullValue' using the {@link #newInstance(Class, boolean)} method. + * Also see {@link BeanPropertyRowMapper#setPrimitivesDefaultedForNullValue(boolean)} + * *

To facilitate mapping between columns and fields that don't have matching names, * try using column aliases in the SQL statement like "select fname as first_name from customer". * @@ -55,8 +59,19 @@ public class ParameterizedBeanPropertyRowMapper extends BeanPropertyRowMapper * @param mappedClass the class that each row should be mapped to */ public static ParameterizedBeanPropertyRowMapper newInstance(Class mappedClass) { + return newInstance(mappedClass, false); + } + + /** + * Static factory method to create a new ParameterizedBeanPropertyRowMapper + * (with the mapped class specified only once). + * @param mappedClass the class that each row should be mapped to + * @param primitivesDefaultedForNullValue whether we're defaulting primitives when mapping a null value + */ + public static ParameterizedBeanPropertyRowMapper newInstance(Class mappedClass, boolean primitivesDefaultedForNullValue) { ParameterizedBeanPropertyRowMapper newInstance = new ParameterizedBeanPropertyRowMapper(); newInstance.setMappedClass(mappedClass); + newInstance.setPrimitivesDefaultedForNullValue(primitivesDefaultedForNullValue); return newInstance; } diff --git a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 271fadee7c..f31d3dd025 100644 --- a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -46,6 +46,8 @@ public abstract class AbstractRowMapperTests extends TestCase { protected MockControl conControl; protected Connection con; + protected MockControl conControl2; + protected Connection con2; protected MockControl rsmdControl; protected ResultSetMetaData rsmd; protected MockControl rsControl; @@ -53,6 +55,13 @@ public abstract class AbstractRowMapperTests extends TestCase { protected MockControl stmtControl; protected Statement stmt; protected JdbcTemplate jdbcTemplate; + protected MockControl rsmdControl2; + protected ResultSetMetaData rsmd2; + protected MockControl rsControl2; + protected ResultSet rs2; + protected MockControl stmtControl2; + protected Statement stmt2; + protected JdbcTemplate jdbcTemplate2; protected void setUp() throws SQLException { conControl = MockControl.createControl(Connection.class); @@ -110,13 +119,75 @@ public abstract class AbstractRowMapperTests extends TestCase { stmt.close(); stmtControl.setVoidCallable(1); + conControl2 = MockControl.createControl(Connection.class); + con2 = (Connection) conControl2.getMock(); + con2.isClosed(); + conControl2.setDefaultReturnValue(false); + + rsmdControl2 = MockControl.createControl(ResultSetMetaData.class); + rsmd2 = (ResultSetMetaData)rsmdControl2.getMock(); + rsmd2.getColumnCount(); + rsmdControl2.setReturnValue(4, 2); + rsmd2.getColumnLabel(1); + rsmdControl2.setReturnValue("name", 2); + rsmd2.getColumnLabel(2); + rsmdControl2.setReturnValue("age", 2); + rsmd2.getColumnLabel(3); + rsmdControl2.setReturnValue("birth_date", 1); + rsmd2.getColumnLabel(4); + rsmdControl2.setReturnValue("balance", 1); + rsmdControl2.replay(); + + rsControl2 = MockControl.createControl(ResultSet.class); + rs2 = (ResultSet) rsControl2.getMock(); + rs2.getMetaData(); + rsControl2.setReturnValue(rsmd2, 2); + rs2.next(); + rsControl2.setReturnValue(true, 2); + rs2.getString(1); + rsControl2.setReturnValue("Bubba", 2); + rs2.wasNull(); + rsControl2.setReturnValue(true, 2); + rs2.getLong(2); + rsControl2.setReturnValue(0, 2); + rs2.getTimestamp(3); + rsControl2.setReturnValue(new Timestamp(1221222L), 1); + rs2.getBigDecimal(4); + rsControl2.setReturnValue(new BigDecimal("1234.56"), 1); + rs2.next(); + rsControl2.setReturnValue(false, 1); + rs2.close(); + rsControl2.setVoidCallable(2); + rsControl2.replay(); + + stmtControl2 = MockControl.createControl(Statement.class); + stmt2 = (Statement) stmtControl2.getMock(); + + con2.createStatement(); + conControl2.setReturnValue(stmt2, 2); + stmt2.executeQuery("select name, null as age, birth_date, balance from people"); + stmtControl2.setReturnValue(rs2, 2); + if (debugEnabled) { + stmt2.getWarnings(); + stmtControl2.setReturnValue(null, 2); + } + stmt2.close(); + stmtControl2.setVoidCallable(2); + conControl.replay(); stmtControl.replay(); + conControl2.replay(); + stmtControl2.replay(); jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(new SingleConnectionDataSource(con, false)); jdbcTemplate.setExceptionTranslator(new SQLStateSQLExceptionTranslator()); jdbcTemplate.afterPropertiesSet(); + + jdbcTemplate2 = new JdbcTemplate(); + jdbcTemplate2.setDataSource(new SingleConnectionDataSource(con2, false)); + jdbcTemplate2.setExceptionTranslator(new SQLStateSQLExceptionTranslator()); + jdbcTemplate2.afterPropertiesSet(); } protected void verifyPerson(Person bean) { @@ -127,6 +198,17 @@ public abstract class AbstractRowMapperTests extends TestCase { assertEquals(new BigDecimal("1234.56"), bean.getBalance()); } + protected void verifyPersonWithZeroAge(Person bean) { + conControl2.verify(); + rsControl2.verify(); + rsmdControl2.verify(); + stmtControl2.verify(); + assertEquals("Bubba", bean.getName()); + assertEquals(0L, bean.getAge()); + assertEquals(new java.util.Date(1221222L), bean.getBirth_date()); + assertEquals(new BigDecimal("1234.56"), bean.getBalance()); + } + protected void verifyConcretePerson(ConcretePerson bean) { verify(); assertEquals("Bubba", bean.getName()); diff --git a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java index e877542898..718ddf4518 100644 --- a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java +++ b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java @@ -23,6 +23,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.core.test.ConcretePerson; import org.springframework.jdbc.core.test.ExtendedPerson; import org.springframework.jdbc.core.test.Person; +import org.springframework.beans.TypeMismatchException; /** * @author Thomas Risberg @@ -89,4 +90,22 @@ public class BeanPropertyRowMapperTests extends AbstractRowMapperTests { } } + public void testMappingNullValue() throws SQLException { + BeanPropertyRowMapper mapper = new BeanPropertyRowMapper(Person.class); + try { + List result1 = jdbcTemplate2.query("select name, null as age, birth_date, balance from people", + mapper); + fail("Should have thrown TypeMismatchException because of null value"); + } + catch (TypeMismatchException ex) { + // expected + } + mapper.setPrimitivesDefaultedForNullValue(true); + List result2 = jdbcTemplate2.query("select name, null as age, birth_date, balance from people", + mapper); + assertEquals(1, result2.size()); + Person bean = (Person) result2.get(0); + verifyPersonWithZeroAge(bean); + } + }