Skip to content

Commit 67838f3

Browse files
committed
Provide all counters in BatchUpdateException
This commit updates JbcTemplate#batchUpdate to provide additional information when one batch fails. Previously, the raw BatchUpdateException was thrown with no way to know what had completed thus far. This commit creates an AggregatedBatchUpdateException that wraps the original BatchUpdateException, yet providing the counters of the batches that ran prior to the exception. In essence, this represents the same state as the return value of the method if no batch fails. AggregateBatchUpdateException exposes the original BatchUpdateException in advanced case, such as checking for a sub-class that may contain additional information. Closes gh-23867
1 parent a55207e commit 67838f3

File tree

3 files changed

+132
-1
lines changed

3 files changed

+132
-1
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.jdbc.core;
18+
19+
import java.sql.BatchUpdateException;
20+
21+
/**
22+
* A {@link BatchUpdateException} that provides additional information about
23+
* batches that were successful prior to one failing.
24+
*
25+
* @author Stephane Nicoll
26+
* @since 6.2
27+
*/
28+
@SuppressWarnings("serial")
29+
public class AggregatedBatchUpdateException extends BatchUpdateException {
30+
31+
private final int[][] successfulUpdateCounts;
32+
33+
private final BatchUpdateException originalException;
34+
35+
/**
36+
* Create an aggregated exception with the batches that have completed prior
37+
* to the given {@code cause}.
38+
* @param successfulUpdateCounts the counts of the batches that run successfully
39+
* @param original the exception this instance aggregates
40+
*/
41+
public AggregatedBatchUpdateException(int[][] successfulUpdateCounts, BatchUpdateException original) {
42+
super(original.getMessage(), original.getSQLState(), original.getErrorCode(),
43+
original.getUpdateCounts(), original.getCause());
44+
this.successfulUpdateCounts = successfulUpdateCounts;
45+
this.originalException = original;
46+
// Copy state of the original exception
47+
setNextException(original.getNextException());
48+
for (Throwable suppressed : original.getSuppressed()) {
49+
addSuppressed(suppressed);
50+
}
51+
}
52+
53+
/**
54+
* Return the batches that have completed successfully, prior to this exception.
55+
* <p>Information about the batch that failed is available via
56+
* {@link #getUpdateCounts()}.
57+
* @return an array containing for each batch another array containing the numbers of
58+
* rows affected by each update in the batch
59+
* @see #getUpdateCounts()
60+
*/
61+
public int[][] getSuccessfulUpdateCounts() {
62+
return this.successfulUpdateCounts;
63+
}
64+
65+
/**
66+
* Return the original {@link BatchUpdateException} that this exception aggregates.
67+
* @return the original exception
68+
*/
69+
public BatchUpdateException getOriginalException() {
70+
return this.originalException;
71+
}
72+
73+
}

spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,13 @@ public <T> int[][] batchUpdate(String sql, final Collection<T> batchArgs, final
11161116
int items = n - ((n % batchSize == 0) ? n / batchSize - 1 : (n / batchSize)) * batchSize;
11171117
logger.trace("Sending SQL batch update #" + batchIdx + " with " + items + " items");
11181118
}
1119-
rowsAffected.add(ps.executeBatch());
1119+
try {
1120+
int[] updateCounts = ps.executeBatch();
1121+
rowsAffected.add(updateCounts);
1122+
}
1123+
catch (BatchUpdateException ex) {
1124+
throw new AggregatedBatchUpdateException(rowsAffected.toArray(int[][]::new), ex);
1125+
}
11201126
}
11211127
}
11221128
else {

spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@
3232
import java.util.Collections;
3333
import java.util.List;
3434
import java.util.Map;
35+
import java.util.function.Consumer;
3536

3637
import javax.sql.DataSource;
3738

39+
import org.assertj.core.data.Index;
3840
import org.junit.jupiter.api.BeforeEach;
3941
import org.junit.jupiter.api.Test;
4042

4143
import org.springframework.dao.DataAccessException;
44+
import org.springframework.dao.DuplicateKeyException;
4245
import org.springframework.dao.InvalidDataAccessApiUsageException;
4346
import org.springframework.jdbc.BadSqlGrammarException;
4447
import org.springframework.jdbc.CannotGetJdbcConnectionException;
@@ -799,6 +802,55 @@ void testBatchUpdateWithCollectionOfObjects() throws Exception {
799802
verify(this.connection, atLeastOnce()).close();
800803
}
801804

805+
@Test
806+
void testBatchUpdateWithBatchFailingHasUpdateCounts() throws Exception {
807+
test3BatchesOf2ItemsFailing(exception -> assertThat(exception).cause()
808+
.isInstanceOfSatisfying(AggregatedBatchUpdateException.class, ex -> {
809+
assertThat(ex.getSuccessfulUpdateCounts()).hasDimensions(1, 2)
810+
.contains(new int[] { 1, 1 }, Index.atIndex(0));
811+
assertThat(ex.getUpdateCounts()).contains(-3, -3);
812+
}));
813+
}
814+
815+
@Test
816+
void testBatchUpdateWithBatchFailingMatchesOriginalException() throws Exception {
817+
test3BatchesOf2ItemsFailing(exception -> assertThat(exception).cause()
818+
.isInstanceOfSatisfying(AggregatedBatchUpdateException.class, ex -> {
819+
BatchUpdateException originalException = ex.getOriginalException();
820+
assertThat(ex.getMessage()).isEqualTo(originalException.getMessage());
821+
assertThat(ex.getCause()).isEqualTo(originalException.getCause());
822+
assertThat(ex.getSQLState()).isEqualTo(originalException.getSQLState());
823+
assertThat(ex.getErrorCode()).isEqualTo(originalException.getErrorCode());
824+
assertThat((Exception) ex.getNextException()).isSameAs(originalException.getNextException());
825+
assertThat(ex.getSuppressed()).isEqualTo(originalException.getSuppressed());
826+
}));
827+
}
828+
829+
void test3BatchesOf2ItemsFailing(Consumer<Exception> exception) throws Exception {
830+
String sql = "INSERT INTO NOSUCHTABLE values (?)";
831+
List<Integer> ids = Arrays.asList(1, 2, 3, 2, 4, 5);
832+
int[] rowsAffected = new int[] {1, 1};
833+
834+
given(this.preparedStatement.executeBatch()).willReturn(rowsAffected).willThrow(new BatchUpdateException(
835+
"duplicate key value violates unique constraint \"NOSUCHTABLE_pkey\" Detail: Key (id)=(2) already exists.",
836+
"23505", 0, new int[] { -3, -3 }));
837+
mockDatabaseMetaData(true);
838+
839+
ParameterizedPreparedStatementSetter<Integer> setter = (ps, argument) -> ps.setInt(1, argument);
840+
JdbcTemplate template = new JdbcTemplate(this.dataSource, false);
841+
842+
assertThatExceptionOfType(DuplicateKeyException.class)
843+
.isThrownBy(() -> template.batchUpdate(sql, ids, 2, setter))
844+
.satisfies(exception);
845+
verify(this.preparedStatement, times(4)).addBatch();
846+
verify(this.preparedStatement).setInt(1, 1);
847+
verify(this.preparedStatement, times(2)).setInt(1, 2);
848+
verify(this.preparedStatement).setInt(1, 3);
849+
verify(this.preparedStatement, times(2)).executeBatch();
850+
verify(this.preparedStatement).close();
851+
verify(this.connection, atLeastOnce()).close();
852+
}
853+
802854
@Test
803855
void testCouldNotGetConnectionForOperationOrExceptionTranslator() throws SQLException {
804856
SQLException sqlException = new SQLException("foo", "07xxx");

0 commit comments

Comments
 (0)