Added batchUpdate method taking a Collection, a batch size and a ParameterizedPreparedStatementSetter as arguments (SPR-6334)

This commit is contained in:
Thomas Risberg 2011-06-05 16:42:24 +00:00
parent cfb387383b
commit 0adcb2ad2e
5 changed files with 290 additions and 44 deletions

View File

@ -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<Object[]> 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 <code>java.sql.Types</code>)
* @return an array containing for each batch another array containing the numbers of rows affected
* by each update in the batch
*/
public <T> int[][] batchUpdate(String sql, Collection<T> batchArgs, int batchSize, ParameterizedPreparedStatementSetter<T> pss);
//-------------------------------------------------------------------------
// Methods dealing with callable statements

View File

@ -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<Object[]> 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 <T> int[][] batchUpdate(String sql, final Collection<T> batchArgs, final int batchSize, final ParameterizedPreparedStatementSetter<T> pss) {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL batch update [" + sql + "] with a batch size of " + batchSize);
}
return execute(sql, new PreparedStatementCallback<int[][]>() {
public int[][] doInPreparedStatement(PreparedStatement ps) throws SQLException {
List<int[]> rowsAffected = new ArrayList<int[]>();
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

View File

@ -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.
*
* <p>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.
*
* <p>Implementations <i>do not</i> 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<T> objs, int batchSize, ParameterizedPreparedStatementSetter<T> pss)
*/
public interface ParameterizedPreparedStatementSetter<T> {
/**
* 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;
}

View File

@ -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<Integer> ids = new ArrayList<Integer>();
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<Integer>() {
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");

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<chapter id="jdbc">
@ -8,9 +8,9 @@
<title>Introduction to Spring Framework JDBC</title>
<para>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.<!--Is this sequence correct, as far as what developer does and doesn't do? Does it adhere to info in the rest of the chapter?
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.<!--Is this sequence correct, as far as what developer does and doesn't do? Does it adhere to info in the rest of the chapter?
--><!--How does JDBC know what connection parameters are if a human does not at some point define them?--><!--TR: OK. I have rewritten this as a table indicating who has what responsibility. --></para>
<table align="left" width="">
@ -1036,7 +1036,8 @@ public class ExecuteAnUpdate {
<section id="jdbc-auto-genereted-keys">
<title>Retrieving auto-generated keys</title>
<para>An <methodname>update()</methodname> convenience method supports<!--Give name of this method. Also indicate *what* is acquiring the primary keys. TR: Changed to *retrieval*.
<para>An <methodname>update()</methodname> convenience method
supports<!--Give name of this method. Also indicate *what* is acquiring the primary keys. TR: Changed to *retrieval*.
The name of the method is *update*.--> 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("");</programlisting>
<classname>SimpleJdbcTemplate</classname>.</para>
<section id="jdbc-advanced-classic">
<title>Batch operations with the JdbcTemplate</title>
<title>Basic batch operations with the JdbcTemplate</title>
<para>You accomplish <classname>JdbcTemplate</classname> batch
processing by implementing two methods of a special interface,
@ -1418,11 +1419,12 @@ dataSource.setPassword("");</programlisting>
</section>
<section id="jdbc-advanced-simple">
<title>Batch operations with the SimpleJdbcTemplate</title>
<title>Batch operations with a List of objects</title>
<para>The <classname>SimpleJdbcTemplate</classname> provides an
alternate way of providing the batch update. Instead of implementing a
special batch interface, you provide all parameter values in the call.
<para>Both the <classname>JdbcTemplate</classname> and the
<classname>NamedParameterJdbcTemplate</classname> 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("");</programlisting>
<para>This example shows a batch update using named parameters:</para>
<para><programlisting language="java">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&lt;Actor&gt; 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("");</programlisting>
<para>The same example using classic JDBC "?" placeholders:</para>
<para><programlisting language="java">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&lt;Actor&gt; actors) {
@ -1474,17 +1476,79 @@ dataSource.setPassword("");</programlisting>
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
}</programlisting>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.</para>
}</programlisting>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.</para>
</section>
<section id="jdbc-advanced-simple">
<title>Batch operations with multiple batches</title>
<para>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 <classname>batchUpdate</classname> 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
<classname>ParameterizedPreparedStatementSetter</classname> 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. </para>
<para>This example shows a batch update using a batch size of
100:</para>
<para><programlisting language="java">public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[][] batchUpdate(final Collection&lt;Actor&gt; actors) {
Collection&lt;Object[]&gt; batch = new ArrayList&lt;Object[]&gt;();
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&lt;Actor&gt;() {
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
}</programlisting>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.</para>
</section>
</section>
@ -1715,9 +1779,9 @@ END;</programlisting>The <code>in_id</code> parameter contains the
<para>The <classname>SimpleJdbcCall</classname> is declared in a similar
manner to the <classname>SimpleJdbcInsert</classname>. 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. <!--Reword preceding: You need not subclass *what?* and you declare *what* in init method? TR: Revised, pplease review.-->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. <!--Reword preceding: You need not subclass *what?* and you declare *what* in init method? TR: Revised, pplease review.-->Following
is an example of a SimpleJdbcCall configuration using the above stored
procedure. The only configuration option, in addition to the
<classname>DataSource</classname>, is the name of the stored
@ -2651,9 +2715,9 @@ clobReader.close();</programlisting>
<para>In addition to the primitive values in the value list, you can
create a <classname>java.util.List</classname> of object arrays. This
list would support multiple expressions defined for the <code>in</code>
clause such as <code>select * from T_ACTOR where (id, last_name) in
((1, 'Johnson'), (2, 'Harrop'))</code>. This
of course requires that your database supports this syntax.</para>
clause such as <code>select * from T_ACTOR where (id, last_name) in ((1,
'Johnson'), (2, 'Harrop'))</code>. This of course requires that your
database supports this syntax.</para>
</section>
<section id="jdbc-complex-types">
@ -2815,7 +2879,7 @@ SqlTypeValue value = new AbstractSqlTypeValue() {
<title>Using HSQL</title>
<para>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 <literal>type</literal> attribute of the
<literal>embedded-database</literal> tag to <literal>HSQL</literal>. 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.
<programlisting>&lt;jdbc:initialize-database data-source="dataSource"
properties or an environment bean), e.g. <programlisting>&lt;jdbc:initialize-database data-source="dataSource"
<emphasis role="bold">enabled="#{systemProperties.INITIALIZE_DATABASE}"</emphasis>&gt;
&lt;jdbc:script location="..."/&gt;
&lt;/jdbc:initialize-database&gt;</programlisting></para>
@ -2979,23 +3042,20 @@ public class DataAccessUnitTestTemplate {
<para>The first option might be easy if the application is in your
control, and not otherwise. Some suggestions for how to implement this
are<itemizedlist>
<listitem>
<para>Make the cache initialize lazily on first usage, which
improves application startup time</para>
</listitem>
<listitem>
<para>Have your cache or a separate component that
initializes the cache implement <code>Lifecycle</code> or
<code>SmartLifecycle</code>. When the application context
starts up a <code>SmartLifecycle</code> can be automatically
started if its <code>autoStartup</code> flag is set,
and a <code>Lifecycle</code> can be started
manually by calling
<para>Have your cache or a separate component that initializes
the cache implement <code>Lifecycle</code> or
<code>SmartLifecycle</code>. When the application context starts
up a <code>SmartLifecycle</code> can be automatically started if
its <code>autoStartup</code> flag is set, and a
<code>Lifecycle</code> can be started manually by calling
<code>ConfigurableApplicationContext.start()</code> on the
enclosing context.
</para>
enclosing context.</para>
</listitem>
<listitem>
@ -3003,10 +3063,9 @@ public class DataAccessUnitTestTemplate {
custom observer mechanism to trigger the cache initialization.
<code>ContextRefreshedEvent</code> 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 <code>SmartLifecycle</code> works by default).</para>
initialized), so that is often a useful hook (this is how the
<code>SmartLifecycle</code> works by default).</para>
</listitem>
</itemizedlist></para>
<para>The second option can also be easy. Some suggestions on how to