Skip to content

Commit 332953c

Browse files
committed
Align BatchUpdateException handling among SQLExceptionTranslator variants
Closes gh-35547
1 parent 1cdd56b commit 332953c

File tree

4 files changed

+136
-75
lines changed

4 files changed

+136
-75
lines changed

spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.jdbc.support;
1818

19+
import java.sql.BatchUpdateException;
1920
import java.sql.SQLDataException;
2021
import java.sql.SQLException;
2122
import java.sql.SQLFeatureNotSupportedException;
@@ -51,7 +52,10 @@
5152
* <p>Falls back to a standard {@link SQLStateSQLExceptionTranslator} if the JDBC
5253
* driver does not actually expose JDBC 4 compliant {@code SQLException} subclasses.
5354
*
54-
* <p>This translator serves as the default translator as of 6.0.
55+
* <p>This translator serves as the default JDBC exception translator as of 6.0.
56+
* As of 6.2.12, it specifically introspects {@link java.sql.BatchUpdateException}
57+
* to look at the underlying exception, analogous to the former default
58+
* {@link SQLErrorCodeSQLExceptionTranslator}.
5559
*
5660
* @author Thomas Risberg
5761
* @author Juergen Hoeller
@@ -69,45 +73,50 @@ public SQLExceptionSubclassTranslator() {
6973
@Override
7074
@Nullable
7175
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
72-
if (ex instanceof SQLTransientException) {
73-
if (ex instanceof SQLTransientConnectionException) {
74-
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
76+
SQLException sqlEx = ex;
77+
if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) {
78+
sqlEx = sqlEx.getNextException();
79+
}
80+
81+
if (sqlEx instanceof SQLTransientException) {
82+
if (sqlEx instanceof SQLTransientConnectionException) {
83+
return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);
7584
}
76-
if (ex instanceof SQLTransactionRollbackException) {
77-
if (SQLStateSQLExceptionTranslator.indicatesCannotAcquireLock(ex.getSQLState())) {
78-
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
85+
if (sqlEx instanceof SQLTransactionRollbackException) {
86+
if (SQLStateSQLExceptionTranslator.indicatesCannotAcquireLock(sqlEx.getSQLState())) {
87+
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
7988
}
80-
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
89+
return new PessimisticLockingFailureException(buildMessage(task, sql, sqlEx), sqlEx);
8190
}
82-
if (ex instanceof SQLTimeoutException) {
83-
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
91+
if (sqlEx instanceof SQLTimeoutException) {
92+
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
8493
}
8594
}
86-
else if (ex instanceof SQLNonTransientException) {
87-
if (ex instanceof SQLNonTransientConnectionException) {
88-
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
95+
else if (sqlEx instanceof SQLNonTransientException) {
96+
if (sqlEx instanceof SQLNonTransientConnectionException) {
97+
return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
8998
}
90-
if (ex instanceof SQLDataException) {
91-
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
99+
if (sqlEx instanceof SQLDataException) {
100+
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
92101
}
93-
if (ex instanceof SQLIntegrityConstraintViolationException) {
94-
if (SQLStateSQLExceptionTranslator.indicatesDuplicateKey(ex.getSQLState(), ex.getErrorCode())) {
95-
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
102+
if (sqlEx instanceof SQLIntegrityConstraintViolationException) {
103+
if (SQLStateSQLExceptionTranslator.indicatesDuplicateKey(sqlEx.getSQLState(), sqlEx.getErrorCode())) {
104+
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
96105
}
97-
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
106+
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
98107
}
99-
if (ex instanceof SQLInvalidAuthorizationSpecException) {
100-
return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex);
108+
if (sqlEx instanceof SQLInvalidAuthorizationSpecException) {
109+
return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
101110
}
102-
if (ex instanceof SQLSyntaxErrorException) {
103-
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
111+
if (sqlEx instanceof SQLSyntaxErrorException) {
112+
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
104113
}
105-
if (ex instanceof SQLFeatureNotSupportedException) {
106-
return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex);
114+
if (sqlEx instanceof SQLFeatureNotSupportedException) {
115+
return new InvalidDataAccessApiUsageException(buildMessage(task, sql, sqlEx), sqlEx);
107116
}
108117
}
109-
else if (ex instanceof SQLRecoverableException) {
110-
return new RecoverableDataAccessException(buildMessage(task, sql, ex), ex);
118+
else if (sqlEx instanceof SQLRecoverableException) {
119+
return new RecoverableDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
111120
}
112121

113122
// Fallback to Spring's own SQL state translation...

spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.jdbc.support;
1818

19+
import java.sql.BatchUpdateException;
1920
import java.sql.SQLException;
2021
import java.util.Set;
2122

@@ -41,7 +42,9 @@
4142
*
4243
* <p>This translator is commonly used as a {@link #setFallbackTranslator fallback}
4344
* behind a primary translator such as {@link SQLErrorCodeSQLExceptionTranslator} or
44-
* {@link SQLExceptionSubclassTranslator}.
45+
* {@link SQLExceptionSubclassTranslator}. As of 6.2.12, it specifically introspects
46+
* {@link java.sql.BatchUpdateException} to look at the underlying exception
47+
* (for alignment when used behind a {@link SQLExceptionSubclassTranslator}).
4548
*
4649
* @author Rod Johnson
4750
* @author Juergen Hoeller
@@ -103,43 +106,60 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException
103106
@Override
104107
@Nullable
105108
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
106-
// First, the getSQLState check...
107-
String sqlState = getSqlState(ex);
109+
SQLException sqlEx = ex;
110+
String sqlState;
111+
if (sqlEx instanceof BatchUpdateException) {
112+
// Unwrap BatchUpdateException to expose contained exception
113+
// with potentially more specific SQL state.
114+
if (sqlEx.getNextException() != null) {
115+
SQLException nestedSqlEx = sqlEx.getNextException();
116+
if (nestedSqlEx.getSQLState() != null) {
117+
sqlEx = nestedSqlEx;
118+
}
119+
}
120+
sqlState = sqlEx.getSQLState();
121+
}
122+
else {
123+
// Expose top-level exception but potentially use nested SQL state.
124+
sqlState = getSqlState(sqlEx);
125+
}
126+
127+
// The actual SQL state check...
108128
if (sqlState != null && sqlState.length() >= 2) {
109129
String classCode = sqlState.substring(0, 2);
110130
if (logger.isDebugEnabled()) {
111131
logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'");
112132
}
113133
if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) {
114-
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
134+
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
115135
}
116136
else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {
117-
if (indicatesDuplicateKey(sqlState, ex.getErrorCode())) {
118-
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
137+
if (indicatesDuplicateKey(sqlState, sqlEx.getErrorCode())) {
138+
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
119139
}
120-
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
140+
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
121141
}
122142
else if (PESSIMISTIC_LOCKING_FAILURE_CODES.contains(classCode)) {
123143
if (indicatesCannotAcquireLock(sqlState)) {
124-
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
144+
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
125145
}
126-
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
146+
return new PessimisticLockingFailureException(buildMessage(task, sql, sqlEx), sqlEx);
127147
}
128148
else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
129149
if (indicatesQueryTimeout(sqlState)) {
130-
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
150+
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
131151
}
132-
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
152+
return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
133153
}
134154
else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {
135-
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
155+
return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);
136156
}
137157
}
138158

139159
// For MySQL: exception class name indicating a timeout?
140160
// (since MySQL doesn't throw the JDBC 4 SQLTimeoutException)
141-
if (ex.getClass().getName().contains("Timeout")) {
142-
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
161+
if (sqlEx.getClass().getName().contains("Timeout")) {
162+
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
143163
}
144164

145165
// Couldn't resolve anything proper - resort to UncategorizedSQLException.

spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,51 +43,58 @@
4343
import org.springframework.dao.TransientDataAccessResourceException;
4444
import org.springframework.jdbc.BadSqlGrammarException;
4545

46-
import static org.assertj.core.api.Assertions.assertThat;
46+
import static org.springframework.jdbc.support.SQLStateSQLExceptionTranslatorTests.buildBatchUpdateException;
4747

4848
/**
4949
* @author Thomas Risberg
5050
* @author Juergen Hoeller
5151
*/
5252
class SQLExceptionSubclassTranslatorTests {
5353

54+
private final SQLExceptionTranslator translator = new SQLExceptionSubclassTranslator();
55+
56+
5457
@Test
5558
void exceptionClassTranslation() {
56-
doTest(new SQLDataException("", "", 0), DataIntegrityViolationException.class);
57-
doTest(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class);
58-
doTest(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class);
59-
doTest(new SQLIntegrityConstraintViolationException("", "23505", 0), DuplicateKeyException.class);
60-
doTest(new SQLIntegrityConstraintViolationException("", "23000", 1), DuplicateKeyException.class);
61-
doTest(new SQLIntegrityConstraintViolationException("", "23000", 1062), DuplicateKeyException.class);
62-
doTest(new SQLIntegrityConstraintViolationException("", "23000", 2601), DuplicateKeyException.class);
63-
doTest(new SQLIntegrityConstraintViolationException("", "23000", 2627), DuplicateKeyException.class);
64-
doTest(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class);
65-
doTest(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class);
66-
doTest(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class);
67-
doTest(new SQLSyntaxErrorException("", "", 0), BadSqlGrammarException.class);
68-
doTest(new SQLTimeoutException("", "", 0), QueryTimeoutException.class);
69-
doTest(new SQLTransactionRollbackException("", "", 0), PessimisticLockingFailureException.class);
70-
doTest(new SQLTransactionRollbackException("", "40001", 0), CannotAcquireLockException.class);
71-
doTest(new SQLTransientConnectionException("", "", 0), TransientDataAccessResourceException.class);
59+
assertTranslation(new SQLDataException("", "", 0), DataIntegrityViolationException.class);
60+
assertTranslation(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class);
61+
assertTranslation(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class);
62+
assertTranslation(new SQLIntegrityConstraintViolationException("", "23505", 0), DuplicateKeyException.class);
63+
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 1), DuplicateKeyException.class);
64+
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 1062), DuplicateKeyException.class);
65+
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 2601), DuplicateKeyException.class);
66+
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 2627), DuplicateKeyException.class);
67+
assertTranslation(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class);
68+
assertTranslation(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class);
69+
assertTranslation(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class);
70+
assertTranslation(new SQLSyntaxErrorException("", "", 0), BadSqlGrammarException.class);
71+
assertTranslation(new SQLTimeoutException("", "", 0), QueryTimeoutException.class);
72+
assertTranslation(new SQLTransactionRollbackException("", "", 0), PessimisticLockingFailureException.class);
73+
assertTranslation(new SQLTransactionRollbackException("", "40001", 0), CannotAcquireLockException.class);
74+
assertTranslation(new SQLTransientConnectionException("", "", 0), TransientDataAccessResourceException.class);
75+
}
76+
77+
@Test
78+
void batchExceptionTranslation() {
79+
assertTranslation(buildBatchUpdateException("JZ", new SQLIntegrityConstraintViolationException("", "23505", 0)),
80+
DuplicateKeyException.class);
81+
assertTranslation(buildBatchUpdateException(null, new SQLIntegrityConstraintViolationException("", "23505", 0)),
82+
DuplicateKeyException.class);
7283
}
7384

7485
@Test
7586
void fallbackStateTranslation() {
7687
// Test fallback. We assume that no database will ever return this error code,
7788
// but 07xxx will be bad grammar picked up by the fallback SQLState translator
78-
doTest(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class);
89+
assertTranslation(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class);
7990
// and 08xxx will be data resource failure (non-transient) picked up by the fallback SQLState translator
80-
doTest(new SQLException("", "08xxx", 666666666), DataAccessResourceFailureException.class);
91+
assertTranslation(new SQLException("", "08xxx", 666666666), DataAccessResourceFailureException.class);
8192
}
8293

8394

84-
private void doTest(SQLException ex, Class<?> dataAccessExceptionType) {
85-
SQLExceptionTranslator translator = new SQLExceptionSubclassTranslator();
86-
DataAccessException dax = translator.translate("task", "SQL", ex);
87-
88-
assertThat(dax).as("Specific translation must not result in null").isNotNull();
89-
assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
90-
assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex);
95+
private void assertTranslation(SQLException ex, Class<?> dataAccessExceptionType) {
96+
DataAccessException dae = translator.translate("task", "SQL", ex);
97+
SQLStateSQLExceptionTranslatorTests.assertTranslation(dae, ex, dataAccessExceptionType);
9198
}
9299

93100
}

spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.jdbc.support;
1818

19+
import java.sql.BatchUpdateException;
1920
import java.sql.SQLException;
2021

2122
import org.junit.jupiter.api.Test;
@@ -45,6 +46,7 @@ class SQLStateSQLExceptionTranslatorTests {
4546

4647
private final SQLExceptionTranslator translator = new SQLStateSQLExceptionTranslator();
4748

49+
4850
@Test
4951
void translateNullException() {
5052
assertThatIllegalArgumentException().isThrownBy(() -> translator.translate("", "", null));
@@ -125,6 +127,16 @@ void translateQueryTimeout() {
125127
assertTranslation("57014", QueryTimeoutException.class);
126128
}
127129

130+
@Test
131+
void translateWithinQualifiedBatch() {
132+
assertTranslation(buildBatchUpdateException("JZ", new SQLException("", "23505", 0)), DuplicateKeyException.class);
133+
}
134+
135+
@Test
136+
void translateWithinUnqualifiedBatch() {
137+
assertTranslation(buildBatchUpdateException(null, new SQLException("", "23505", 0)), DuplicateKeyException.class);
138+
}
139+
128140
@Test
129141
void translateUncategorized() {
130142
assertTranslation("00000000", null);
@@ -142,28 +154,41 @@ void invalidSqlStateCode() {
142154
*/
143155
@Test
144156
void malformedSqlStateCodes() {
145-
assertTranslation(null, null);
157+
assertTranslation((String) null, null);
146158
assertTranslation("", null);
147159
assertTranslation("I", null);
148160
}
149161

150162

151163
private void assertTranslation(@Nullable String sqlState, @Nullable Class<?> dataAccessExceptionType) {
152-
assertTranslation(sqlState, 0, dataAccessExceptionType);
164+
assertTranslation(new SQLException("reason", sqlState, 0), dataAccessExceptionType);
153165
}
154166

155167
private void assertTranslation(@Nullable String sqlState, int errorCode, @Nullable Class<?> dataAccessExceptionType) {
156-
SQLException ex = new SQLException("reason", sqlState, errorCode);
157-
DataAccessException dax = translator.translate("task", "SQL", ex);
168+
assertTranslation(new SQLException("reason", sqlState, errorCode), dataAccessExceptionType);
169+
}
170+
171+
private void assertTranslation(SQLException ex, @Nullable Class<?> dataAccessExceptionType) {
172+
DataAccessException dae = translator.translate("task", "SQL", ex);
158173

159174
if (dataAccessExceptionType == null) {
160-
assertThat(dax).as("Expected translation to null").isNull();
175+
assertThat(dae).as("Expected translation to null").isNull();
161176
return;
162177
}
178+
assertTranslation(dae, ex, dataAccessExceptionType);
179+
}
180+
181+
static void assertTranslation(DataAccessException dae, SQLException ex, Class<?> dataAccessExceptionType) {
182+
assertThat(dae).as("Specific translation must not result in null").isNotNull();
183+
assertThat(dae).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
184+
assertThat(dae.getCause()).as("The exact same original SQLException must be preserved").isSameAs(
185+
ex instanceof BatchUpdateException bue ? bue.getNextException() : ex);
186+
}
163187

164-
assertThat(dax).as("Specific translation must not result in null").isNotNull();
165-
assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
166-
assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex);
188+
static BatchUpdateException buildBatchUpdateException(@Nullable String sqlState, SQLException next) {
189+
BatchUpdateException ex = new BatchUpdateException("", sqlState, null);
190+
ex.setNextException(next);
191+
return ex;
167192
}
168193

169194
}

0 commit comments

Comments
 (0)