From 0adcb2ad2ea5bdf85d27e9c279cf165db1eeb33c Mon Sep 17 00:00:00 2001 From: Thomas Risberg Date: Sun, 5 Jun 2011 16:42:24 +0000 Subject: [PATCH] Added batchUpdate method taking a Collection, a batch size and a ParameterizedPreparedStatementSetter as arguments (SPR-6334) --- .../jdbc/core/JdbcOperations.java | 15 +- .../jdbc/core/JdbcTemplate.java | 55 ++++++- .../ParameterizedPreparedStatementSetter.java | 50 ++++++ .../jdbc/core/JdbcTemplateTests.java | 71 +++++++++ spring-framework-reference/src/jdbc.xml | 143 +++++++++++++----- 5 files changed, 290 insertions(+), 44 deletions(-) create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterizedPreparedStatementSetter.java diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java index ae04d0c968..d238d742db 100644 --- a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -16,6 +16,7 @@ package org.springframework.jdbc.core; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -1004,7 +1005,19 @@ public interface JdbcOperations { * @return an array containing the numbers of rows affected by each update in the batch */ public int[] batchUpdate(String sql, List batchArgs, int[] argTypes); - + + /** + * Execute multiple batches using the supplied SQL statement with the collect of supplied arguments. + * The arguments' values will be set using the ParameterizedPreparedStatementSetter. + * Each batch should be of size indicated in 'batchSize'. + * @param sql the SQL statement to execute. + * @param batchArgs the List of Object arrays containing the batch of arguments for the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @return an array containing for each batch another array containing the numbers of rows affected + * by each update in the batch + */ + public int[][] batchUpdate(String sql, Collection batchArgs, int batchSize, ParameterizedPreparedStatementSetter pss); //------------------------------------------------------------------------- // Methods dealing with callable statements diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 8d4ecb71d3..e413f99279 100644 --- a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -28,6 +28,7 @@ import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -929,7 +930,59 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { public int[] batchUpdate(String sql, List batchArgs, int[] argTypes) { return BatchUpdateUtils.executeBatchUpdate(sql, batchArgs, argTypes, this); } - + + /* + * (non-Javadoc) + * @see org.springframework.jdbc.core.JdbcOperations#batchUpdate(java.lang.String, java.util.Collection, int, org.springframework.jdbc.core.ParameterizedPreparedStatementSetter) + * + * Contribution by Nicolas Fabre + */ + public int[][] batchUpdate(String sql, final Collection batchArgs, final int batchSize, final ParameterizedPreparedStatementSetter pss) { + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL batch update [" + sql + "] with a batch size of " + batchSize); + } + return execute(sql, new PreparedStatementCallback() { + public int[][] doInPreparedStatement(PreparedStatement ps) throws SQLException { + List rowsAffected = new ArrayList(); + try { + boolean batchSupported = true; + if (!JdbcUtils.supportsBatchUpdates(ps.getConnection())) { + batchSupported = false; + logger.warn("JDBC Driver does not support Batch updates; resorting to single statement execution"); + } + int n = 0; + for (T obj : batchArgs) { + pss.setValues(ps, obj); + n++; + if (batchSupported) { + ps.addBatch(); + if (n % batchSize == 0 || n == batchArgs.size()) { + if (logger.isDebugEnabled()) { + int batchIdx = (n % batchSize == 0) ? n / batchSize : (n / batchSize) + 1; + int items = n - ((n % batchSize == 0) ? n / batchSize - 1 : (n / batchSize)) * batchSize; + logger.debug("Sending SQL batch update #" + batchIdx + " with " + items + " items"); + } + rowsAffected.add(ps.executeBatch()); + } + } + else { + int i = ps.executeUpdate(); + rowsAffected.add(new int[] {i}); + } + } + int[][] result = new int[rowsAffected.size()][]; + for (int i = 0; i < result.length; i++) { + result[i] = rowsAffected.get(i); + } + return result; + } finally { + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + } + } + }); + } //------------------------------------------------------------------------- // Methods dealing with callable statements diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterizedPreparedStatementSetter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterizedPreparedStatementSetter.java new file mode 100644 index 0000000000..63d8b5e3f8 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterizedPreparedStatementSetter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Parameterized callback interface used by the {@link JdbcTemplate} class for batch updates. + * + *

This interface sets values on a {@link java.sql.PreparedStatement} provided + * by the JdbcTemplate class, for each of a number of updates in a batch using the + * same SQL. Implementations are responsible for setting any necessary parameters. + * SQL with placeholders will already have been supplied. + * + *

Implementations do not need to concern themselves with SQLExceptions + * that may be thrown from operations they attempt. The JdbcTemplate class will + * catch and handle SQLExceptions appropriately. + * + * @author Nicolas Fabre + * @author Thomas Risberg + * @since 3.1 + * @see JdbcTemplate#batchUpdate(String sql, Collection objs, int batchSize, ParameterizedPreparedStatementSetter pss) + */ +public interface ParameterizedPreparedStatementSetter { + + /** + * Set parameter values on the given PreparedStatement. + * + * @param ps the PreparedStatement to invoke setter methods on + * @param argument the object containing the values to be set + * @throws SQLException if a SQLException is encountered (i.e. there is no need to catch SQLException) + */ + void setValues(PreparedStatement ps, T argument) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java index cae6253653..bfe4c14824 100644 --- a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java +++ b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java @@ -26,6 +26,7 @@ import java.sql.SQLWarning; import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -1189,6 +1190,76 @@ public class JdbcTemplateTests extends AbstractJdbcTests { BatchUpdateTestHelper.verifyBatchUpdateMocks(ctrlPreparedStatement, ctrlDatabaseMetaData); } + public void testBatchUpdateWithCollectionOfObjects() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final List ids = new ArrayList(); + ids.add(Integer.valueOf(100)); + ids.add(Integer.valueOf(200)); + ids.add(Integer.valueOf(300)); + final int[] rowsAffected1 = new int[] { 1, 2 }; + final int[] rowsAffected2 = new int[] { 3 }; + + MockControl ctrlPreparedStatement = MockControl.createControl(PreparedStatement.class); + PreparedStatement mockPreparedStatement = (PreparedStatement) ctrlPreparedStatement.getMock(); + mockPreparedStatement.getConnection(); + ctrlPreparedStatement.setReturnValue(mockConnection); + mockPreparedStatement.setInt(1, ids.get(0)); + ctrlPreparedStatement.setVoidCallable(); + mockPreparedStatement.addBatch(); + ctrlPreparedStatement.setVoidCallable(); + mockPreparedStatement.setInt(1, ids.get(1)); + ctrlPreparedStatement.setVoidCallable(); + mockPreparedStatement.addBatch(); + ctrlPreparedStatement.setVoidCallable(); + mockPreparedStatement.setInt(1, ids.get(2)); + ctrlPreparedStatement.setVoidCallable(); + mockPreparedStatement.executeBatch(); + ctrlPreparedStatement.setReturnValue(rowsAffected1); + mockPreparedStatement.addBatch(); + ctrlPreparedStatement.setVoidCallable(); + mockPreparedStatement.executeBatch(); + ctrlPreparedStatement.setReturnValue(rowsAffected2); + if (debugEnabled) { + mockPreparedStatement.getWarnings(); + ctrlPreparedStatement.setReturnValue(null); + } + mockPreparedStatement.close(); + ctrlPreparedStatement.setVoidCallable(); + + MockControl ctrlDatabaseMetaData = MockControl.createControl(DatabaseMetaData.class); + DatabaseMetaData mockDatabaseMetaData = (DatabaseMetaData) ctrlDatabaseMetaData.getMock(); + mockDatabaseMetaData.getDatabaseProductName(); + ctrlDatabaseMetaData.setReturnValue("MySQL"); + mockDatabaseMetaData.supportsBatchUpdates(); + ctrlDatabaseMetaData.setReturnValue(true); + + mockConnection.prepareStatement(sql); + ctrlConnection.setReturnValue(mockPreparedStatement); + mockConnection.getMetaData(); + ctrlConnection.setReturnValue(mockDatabaseMetaData, 2); + + ctrlPreparedStatement.replay(); + ctrlDatabaseMetaData.replay(); + replay(); + + ParameterizedPreparedStatementSetter setter = new ParameterizedPreparedStatementSetter() { + public void setValues(PreparedStatement ps, Integer argument) throws SQLException { + ps.setInt(1, argument.intValue()); + } + }; + + JdbcTemplate template = new JdbcTemplate(mockDataSource, false); + + int[][] actualRowsAffected = template.batchUpdate(sql, ids, 2, setter); + assertTrue("executed 2 updates", actualRowsAffected[0].length == 2); + assertEquals(rowsAffected1[0], actualRowsAffected[0][0]); + assertEquals(rowsAffected1[1], actualRowsAffected[0][1]); + assertEquals(rowsAffected2[0], actualRowsAffected[1][0]); + + ctrlPreparedStatement.verify(); + ctrlDatabaseMetaData.verify(); + } + public void testCouldntGetConnectionOrExceptionTranslator() throws SQLException { SQLException sex = new SQLException("foo", "07xxx"); diff --git a/spring-framework-reference/src/jdbc.xml b/spring-framework-reference/src/jdbc.xml index ea03224744..db70eb2dff 100644 --- a/spring-framework-reference/src/jdbc.xml +++ b/spring-framework-reference/src/jdbc.xml @@ -1,4 +1,4 @@ - + @@ -8,9 +8,9 @@ Introduction to Spring Framework JDBC The value-add provided by the Spring Framework JDBC abstraction is - perhaps best shown by the sequence of actions outlined in the table - below. The table shows what actions Spring will take care of and which - actions are the responsibility of you, the application developer. @@ -1036,7 +1036,8 @@ public class ExecuteAnUpdate {
Retrieving auto-generated keys - An update() convenience method supports the retrieval of primary keys generated by the database. This support is part of the JDBC 3.0 standard; see Chapter 13.6 of the specification for details. The method takes a @@ -1368,7 +1369,7 @@ dataSource.setPassword(""); SimpleJdbcTemplate.
- Batch operations with the JdbcTemplate + Basic batch operations with the JdbcTemplate You accomplish JdbcTemplate batch processing by implementing two methods of a special interface, @@ -1418,11 +1419,12 @@ dataSource.setPassword("");
- Batch operations with the SimpleJdbcTemplate + Batch operations with a List of objects - The SimpleJdbcTemplate provides an - alternate way of providing the batch update. Instead of implementing a - special batch interface, you provide all parameter values in the call. + Both the JdbcTemplate and the + NamedParameterJdbcTemplate provides an alternate + way of providing the batch update. Instead of implementing a special + batch interface, you provide all parameter values in the call as a list. The framework loops over these values and uses an internal prepared statement setter. The API varies depending on whether you use named parameters. For the named parameters you provide an array of @@ -1435,15 +1437,15 @@ dataSource.setPassword(""); This example shows a batch update using named parameters: public class JdbcActorDao implements ActorDao { - private SimpleJdbcTemplate simpleJdbcTemplate; + private NamedParameterTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { - this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int[] batchUpdate(final List<Actor> actors) { SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(actors.toArray()); - int[] updateCounts = simpleJdbcTemplate.batchUpdate( + int[] updateCounts = namedParameterJdbcTemplate.batchUpdate( "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", batch); return updateCounts; @@ -1459,10 +1461,10 @@ dataSource.setPassword(""); The same example using classic JDBC "?" placeholders: public class JdbcActorDao implements ActorDao { - private SimpleJdbcTemplate simpleJdbcTemplate; + private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { - this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + this.jdbcTemplate = new JdbcTemplate(dataSource); } public int[] batchUpdate(final List<Actor> actors) { @@ -1474,17 +1476,79 @@ dataSource.setPassword(""); actor.getId()}; batch.add(values); } - int[] updateCounts = simpleJdbcTemplate.batchUpdate( + int[] updateCounts = jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", batch); return updateCounts; } // ... additional methods -}All batch update methods return an int array containing the - number of affected rows for each batch entry. This count is reported by - the JDBC driver. If the count is not available, the JDBC driver returns - a -2 value. +}All of the above batch update methods return an int array + containing the number of affected rows for each batch entry. This count + is reported by the JDBC driver. If the count is not available, the JDBC + driver returns a -2 value. +
+ +
+ Batch operations with multiple batches + + The last example of a batch update deals with batches that are so + large that you want to break them up into several smaller batches. You + can of course do this with the methods mentioned above by making + multiple calls to the batchUpdate method, but + there is now a more convenient method. This method takes, in addition to + the SQL statement, a Collection of objects containing the parameters, + the number of updates to make for each batch and a + ParameterizedPreparedStatementSetter to set the + values for the parameters of the prepared statement. The framework loops + over the provided values and breaks the update calls into batches of the + size specified. + + This example shows a batch update using a batch size of + 100: + + public class JdbcActorDao implements ActorDao { + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public int[][] batchUpdate(final Collection<Actor> actors) { + Collection<Object[]> batch = new ArrayList<Object[]>(); + for (Actor actor : actors) { + Object[] values = new Object[] { + actor.getFirstName(), + actor.getLastName(), + actor.getId()}; + batch.add(values); + } + int[][] updateCounts = jdbcTemplate.batchUpdate( + "update t_actor set first_name = ?, last_name = ? where id = ?", + actors, + 100, + new ParameterizedPreparedStatementSetter<Actor>() { + public void setValues(PreparedStatement ps, Actor argument) throws SQLException { + ps.setString(1, argument.getFirstName()); + ps.setString(2, argument.getLastName()); + ps.setLong(3, argument.getId().longValue()); + + } + } ); + return updateCounts; + } + + // ... additional methods +}The batch update methods for this call returns an array of + int arrays containing an array entry for each batch with an array of the + number of affected rows for each update. The top level array's length + indicates the number of batches executed and the second level array's + length indicates the number of updates in that batch. The number of + updates in each batch should be the the batch size provided for all + batches except for the last one that might be less, depending on the + total number of updat objects provided. The update count for each update + stament is the one reported by the JDBC driver. If the count is not + available, the JDBC driver returns a -2 value.
@@ -1715,9 +1779,9 @@ END;The in_id parameter contains the The SimpleJdbcCall is declared in a similar manner to the SimpleJdbcInsert. You should instantiate and configure the class in the initialization method of your - data access layer. Compared to the StoredProcedure class, you don't - have to create a subclass and you don't have to declare parameters that - can be looked up in the database metadata. Following + data access layer. Compared to the StoredProcedure class, you don't have + to create a subclass and you don't have to declare parameters that can + be looked up in the database metadata. Following is an example of a SimpleJdbcCall configuration using the above stored procedure. The only configuration option, in addition to the DataSource, is the name of the stored @@ -2651,9 +2715,9 @@ clobReader.close(); In addition to the primitive values in the value list, you can create a java.util.List of object arrays. This list would support multiple expressions defined for the in - clause such as select * from T_ACTOR where (id, last_name) in - ((1, 'Johnson'), (2, 'Harrop')). This - of course requires that your database supports this syntax. + clause such as select * from T_ACTOR where (id, last_name) in ((1, + 'Johnson'), (2, 'Harrop')). This of course requires that your + database supports this syntax.
@@ -2815,7 +2879,7 @@ SqlTypeValue value = new AbstractSqlTypeValue() { Using HSQL Spring supports HSQL 1.8.0 and above. HSQL is the default embedded - database if no type is specified explicitly. To specify HSQL explicitly, + database if no type is specified explicitly. To specify HSQL explicitly, set the type attribute of the embedded-database tag to HSQL. If you are using the builder API, call the @@ -2920,8 +2984,7 @@ public class DataAccessUnitTestTemplate { existing data, the XML namespace provides a couple more options. The first is flag to switch the initialization on and off. This can be set according to the environment (e.g. to pull a boolean value from system - properties or an environment bean), e.g. - <jdbc:initialize-database data-source="dataSource" + properties or an environment bean), e.g. <jdbc:initialize-database data-source="dataSource" enabled="#{systemProperties.INITIALIZE_DATABASE}"> <jdbc:script location="..."/> </jdbc:initialize-database> @@ -2979,23 +3042,20 @@ public class DataAccessUnitTestTemplate { The first option might be easy if the application is in your control, and not otherwise. Some suggestions for how to implement this are - Make the cache initialize lazily on first usage, which improves application startup time - Have your cache or a separate component that - initializes the cache implement Lifecycle or - SmartLifecycle. When the application context - starts up a SmartLifecycle can be automatically - started if its autoStartup flag is set, - and a Lifecycle can be started - manually by calling + Have your cache or a separate component that initializes + the cache implement Lifecycle or + SmartLifecycle. When the application context starts + up a SmartLifecycle can be automatically started if + its autoStartup flag is set, and a + Lifecycle can be started manually by calling ConfigurableApplicationContext.start() on the - enclosing context. - + enclosing context. @@ -3003,10 +3063,9 @@ public class DataAccessUnitTestTemplate { custom observer mechanism to trigger the cache initialization. ContextRefreshedEvent is always published by the context when it is ready for use (after all beans have been - initialized), so that is often a useful hook (this is - how the SmartLifecycle works by default). + initialized), so that is often a useful hook (this is how the + SmartLifecycle works by default). - The second option can also be easy. Some suggestions on how to