diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/AggregatedBatchUpdateException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/AggregatedBatchUpdateException.java new file mode 100644 index 00000000000..671eacb2e1c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/AggregatedBatchUpdateException.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.BatchUpdateException; + +/** + * A {@link BatchUpdateException} that provides additional information about + * batches that were successful prior to one failing. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public class AggregatedBatchUpdateException extends BatchUpdateException { + + private final int[][] successfulUpdateCounts; + + private final BatchUpdateException originalException; + + /** + * Create an aggregated exception with the batches that have completed prior + * to the given {@code cause}. + * @param successfulUpdateCounts the counts of the batches that run successfully + * @param original the exception this instance aggregates + */ + public AggregatedBatchUpdateException(int[][] successfulUpdateCounts, BatchUpdateException original) { + super(original.getMessage(), original.getSQLState(), original.getErrorCode(), + original.getUpdateCounts(), original.getCause()); + this.successfulUpdateCounts = successfulUpdateCounts; + this.originalException = original; + // Copy state of the original exception + setNextException(original.getNextException()); + for (Throwable suppressed : original.getSuppressed()) { + addSuppressed(suppressed); + } + } + + /** + * Return the batches that have completed successfully, prior to this exception. + *
Information about the batch that failed is available via
+ * {@link #getUpdateCounts()}.
+ * @return an array containing for each batch another array containing the numbers of
+ * rows affected by each update in the batch
+ * @see #getUpdateCounts()
+ */
+ public int[][] getSuccessfulUpdateCounts() {
+ return this.successfulUpdateCounts;
+ }
+
+ /**
+ * Return the original {@link BatchUpdateException} that this exception aggregates.
+ * @return the original exception
+ */
+ public BatchUpdateException getOriginalException() {
+ return this.originalException;
+ }
+
+}
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
index aad9bc35428..1b570bc5155 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
@@ -1116,7 +1116,13 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
int items = n - ((n % batchSize == 0) ? n / batchSize - 1 : (n / batchSize)) * batchSize;
logger.trace("Sending SQL batch update #" + batchIdx + " with " + items + " items");
}
- rowsAffected.add(ps.executeBatch());
+ try {
+ int[] updateCounts = ps.executeBatch();
+ rowsAffected.add(updateCounts);
+ }
+ catch (BatchUpdateException ex) {
+ throw new AggregatedBatchUpdateException(rowsAffected.toArray(int[][]::new), ex);
+ }
}
}
else {
diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java
index 70e15f55316..4848f4709fe 100644
--- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java
+++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java
@@ -32,13 +32,16 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import javax.sql.DataSource;
+import org.assertj.core.data.Index;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
@@ -799,6 +802,55 @@ class JdbcTemplateTests {
verify(this.connection, atLeastOnce()).close();
}
+ @Test
+ void testBatchUpdateWithBatchFailingHasUpdateCounts() throws Exception {
+ test3BatchesOf2ItemsFailing(exception -> assertThat(exception).cause()
+ .isInstanceOfSatisfying(AggregatedBatchUpdateException.class, ex -> {
+ assertThat(ex.getSuccessfulUpdateCounts()).hasDimensions(1, 2)
+ .contains(new int[] { 1, 1 }, Index.atIndex(0));
+ assertThat(ex.getUpdateCounts()).contains(-3, -3);
+ }));
+ }
+
+ @Test
+ void testBatchUpdateWithBatchFailingMatchesOriginalException() throws Exception {
+ test3BatchesOf2ItemsFailing(exception -> assertThat(exception).cause()
+ .isInstanceOfSatisfying(AggregatedBatchUpdateException.class, ex -> {
+ BatchUpdateException originalException = ex.getOriginalException();
+ assertThat(ex.getMessage()).isEqualTo(originalException.getMessage());
+ assertThat(ex.getCause()).isEqualTo(originalException.getCause());
+ assertThat(ex.getSQLState()).isEqualTo(originalException.getSQLState());
+ assertThat(ex.getErrorCode()).isEqualTo(originalException.getErrorCode());
+ assertThat((Exception) ex.getNextException()).isSameAs(originalException.getNextException());
+ assertThat(ex.getSuppressed()).isEqualTo(originalException.getSuppressed());
+ }));
+ }
+
+ void test3BatchesOf2ItemsFailing(Consumer