Merge branch '6.2.x'
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions Details
Deploy Docs / Dispatch docs deployment (push) Waiting to run Details

# Conflicts:
#	spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java
#	spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java
This commit is contained in:
Juergen Hoeller 2025-10-03 14:54:04 +02:00
commit 54b43d4a88
4 changed files with 136 additions and 75 deletions

View File

@ -16,6 +16,7 @@
package org.springframework.jdbc.support;
import java.sql.BatchUpdateException;
import java.sql.SQLDataException;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
@ -52,7 +53,10 @@ import org.springframework.jdbc.BadSqlGrammarException;
* <p>Falls back to a standard {@link SQLStateSQLExceptionTranslator} if the JDBC
* driver does not actually expose JDBC 4 compliant {@code SQLException} subclasses.
*
* <p>This translator serves as the default translator as of 6.0.
* <p>This translator serves as the default JDBC exception translator as of 6.0.
* As of 6.2.12, it specifically introspects {@link java.sql.BatchUpdateException}
* to look at the underlying exception, analogous to the former default
* {@link SQLErrorCodeSQLExceptionTranslator}.
*
* @author Thomas Risberg
* @author Juergen Hoeller
@ -69,45 +73,50 @@ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLException
@Override
protected @Nullable DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
if (ex instanceof SQLTransientException) {
if (ex instanceof SQLTransientConnectionException) {
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
SQLException sqlEx = ex;
if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) {
sqlEx = sqlEx.getNextException();
}
if (ex instanceof SQLTransactionRollbackException) {
if (SQLStateSQLExceptionTranslator.indicatesCannotAcquireLock(ex.getSQLState())) {
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
if (sqlEx instanceof SQLTransientException) {
if (sqlEx instanceof SQLTransientConnectionException) {
return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);
}
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
if (sqlEx instanceof SQLTransactionRollbackException) {
if (SQLStateSQLExceptionTranslator.indicatesCannotAcquireLock(sqlEx.getSQLState())) {
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
}
if (ex instanceof SQLTimeoutException) {
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
return new PessimisticLockingFailureException(buildMessage(task, sql, sqlEx), sqlEx);
}
if (sqlEx instanceof SQLTimeoutException) {
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
}
}
else if (ex instanceof SQLNonTransientException) {
if (ex instanceof SQLNonTransientConnectionException) {
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
else if (sqlEx instanceof SQLNonTransientException) {
if (sqlEx instanceof SQLNonTransientConnectionException) {
return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
}
if (ex instanceof SQLDataException) {
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
if (sqlEx instanceof SQLDataException) {
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
}
if (ex instanceof SQLIntegrityConstraintViolationException) {
if (SQLStateSQLExceptionTranslator.indicatesDuplicateKey(ex.getSQLState(), ex.getErrorCode())) {
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
if (sqlEx instanceof SQLIntegrityConstraintViolationException) {
if (SQLStateSQLExceptionTranslator.indicatesDuplicateKey(sqlEx.getSQLState(), sqlEx.getErrorCode())) {
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
}
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
}
if (ex instanceof SQLInvalidAuthorizationSpecException) {
return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex);
if (sqlEx instanceof SQLInvalidAuthorizationSpecException) {
return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
}
if (ex instanceof SQLSyntaxErrorException) {
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
if (sqlEx instanceof SQLSyntaxErrorException) {
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
}
if (ex instanceof SQLFeatureNotSupportedException) {
return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex);
if (sqlEx instanceof SQLFeatureNotSupportedException) {
return new InvalidDataAccessApiUsageException(buildMessage(task, sql, sqlEx), sqlEx);
}
}
else if (ex instanceof SQLRecoverableException) {
return new RecoverableDataAccessException(buildMessage(task, sql, ex), ex);
else if (sqlEx instanceof SQLRecoverableException) {
return new RecoverableDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
}
// Fallback to Spring's own SQL state translation...

View File

@ -16,6 +16,7 @@
package org.springframework.jdbc.support;
import java.sql.BatchUpdateException;
import java.sql.SQLException;
import java.util.Set;
@ -42,7 +43,9 @@ import org.springframework.jdbc.BadSqlGrammarException;
*
* <p>This translator is commonly used as a {@link #setFallbackTranslator fallback}
* behind a primary translator such as {@link SQLErrorCodeSQLExceptionTranslator} or
* {@link SQLExceptionSubclassTranslator}.
* {@link SQLExceptionSubclassTranslator}. As of 6.2.12, it specifically introspects
* {@link java.sql.BatchUpdateException} to look at the underlying exception
* (for alignment when used behind a {@link SQLExceptionSubclassTranslator}).
*
* @author Rod Johnson
* @author Juergen Hoeller
@ -103,43 +106,60 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException
@Override
protected @Nullable DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
// First, the getSQLState check...
String sqlState = getSqlState(ex);
SQLException sqlEx = ex;
String sqlState;
if (sqlEx instanceof BatchUpdateException) {
// Unwrap BatchUpdateException to expose contained exception
// with potentially more specific SQL state.
if (sqlEx.getNextException() != null) {
SQLException nestedSqlEx = sqlEx.getNextException();
if (nestedSqlEx.getSQLState() != null) {
sqlEx = nestedSqlEx;
}
}
sqlState = sqlEx.getSQLState();
}
else {
// Expose top-level exception but potentially use nested SQL state.
sqlState = getSqlState(sqlEx);
}
// The actual SQL state check...
if (sqlState != null && sqlState.length() >= 2) {
String classCode = sqlState.substring(0, 2);
if (logger.isDebugEnabled()) {
logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'");
}
if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) {
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
}
else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {
if (indicatesDuplicateKey(sqlState, ex.getErrorCode())) {
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
if (indicatesDuplicateKey(sqlState, sqlEx.getErrorCode())) {
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
}
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (PESSIMISTIC_LOCKING_FAILURE_CODES.contains(classCode)) {
if (indicatesCannotAcquireLock(sqlState)) {
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
}
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
return new PessimisticLockingFailureException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
if (indicatesQueryTimeout(sqlState)) {
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
}
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);
}
}
// For MySQL: exception class name indicating a timeout?
// (since MySQL doesn't throw the JDBC 4 SQLTimeoutException)
if (ex.getClass().getName().contains("Timeout")) {
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
if (sqlEx.getClass().getName().contains("Timeout")) {
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
}
// Couldn't resolve anything proper - resort to UncategorizedSQLException.

View File

@ -43,7 +43,7 @@ import org.springframework.dao.RecoverableDataAccessException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.jdbc.BadSqlGrammarException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.jdbc.support.SQLStateSQLExceptionTranslatorTests.buildBatchUpdateException;
/**
* @author Thomas Risberg
@ -51,43 +51,50 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class SQLExceptionSubclassTranslatorTests {
private final SQLExceptionTranslator translator = new SQLExceptionSubclassTranslator();
@Test
void exceptionClassTranslation() {
doTest(new SQLDataException("", "", 0), DataIntegrityViolationException.class);
doTest(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class);
doTest(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class);
doTest(new SQLIntegrityConstraintViolationException("", "23505", 0), DuplicateKeyException.class);
doTest(new SQLIntegrityConstraintViolationException("", "23000", 1), DuplicateKeyException.class);
doTest(new SQLIntegrityConstraintViolationException("", "23000", 1062), DuplicateKeyException.class);
doTest(new SQLIntegrityConstraintViolationException("", "23000", 2601), DuplicateKeyException.class);
doTest(new SQLIntegrityConstraintViolationException("", "23000", 2627), DuplicateKeyException.class);
doTest(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class);
doTest(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class);
doTest(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class);
doTest(new SQLSyntaxErrorException("", "", 0), BadSqlGrammarException.class);
doTest(new SQLTimeoutException("", "", 0), QueryTimeoutException.class);
doTest(new SQLTransactionRollbackException("", "", 0), PessimisticLockingFailureException.class);
doTest(new SQLTransactionRollbackException("", "40001", 0), CannotAcquireLockException.class);
doTest(new SQLTransientConnectionException("", "", 0), TransientDataAccessResourceException.class);
assertTranslation(new SQLDataException("", "", 0), DataIntegrityViolationException.class);
assertTranslation(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class);
assertTranslation(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class);
assertTranslation(new SQLIntegrityConstraintViolationException("", "23505", 0), DuplicateKeyException.class);
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 1), DuplicateKeyException.class);
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 1062), DuplicateKeyException.class);
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 2601), DuplicateKeyException.class);
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 2627), DuplicateKeyException.class);
assertTranslation(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class);
assertTranslation(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class);
assertTranslation(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class);
assertTranslation(new SQLSyntaxErrorException("", "", 0), BadSqlGrammarException.class);
assertTranslation(new SQLTimeoutException("", "", 0), QueryTimeoutException.class);
assertTranslation(new SQLTransactionRollbackException("", "", 0), PessimisticLockingFailureException.class);
assertTranslation(new SQLTransactionRollbackException("", "40001", 0), CannotAcquireLockException.class);
assertTranslation(new SQLTransientConnectionException("", "", 0), TransientDataAccessResourceException.class);
}
@Test
void batchExceptionTranslation() {
assertTranslation(buildBatchUpdateException("JZ", new SQLIntegrityConstraintViolationException("", "23505", 0)),
DuplicateKeyException.class);
assertTranslation(buildBatchUpdateException(null, new SQLIntegrityConstraintViolationException("", "23505", 0)),
DuplicateKeyException.class);
}
@Test
void fallbackStateTranslation() {
// Test fallback. We assume that no database will ever return this error code,
// but 07xxx will be bad grammar picked up by the fallback SQLState translator
doTest(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class);
assertTranslation(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class);
// and 08xxx will be data resource failure (non-transient) picked up by the fallback SQLState translator
doTest(new SQLException("", "08xxx", 666666666), DataAccessResourceFailureException.class);
assertTranslation(new SQLException("", "08xxx", 666666666), DataAccessResourceFailureException.class);
}
private void doTest(SQLException ex, Class<?> dataAccessExceptionType) {
SQLExceptionTranslator translator = new SQLExceptionSubclassTranslator();
DataAccessException dax = translator.translate("task", "SQL", ex);
assertThat(dax).as("Specific translation must not result in null").isNotNull();
assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex);
private void assertTranslation(SQLException ex, Class<?> dataAccessExceptionType) {
DataAccessException dae = translator.translate("task", "SQL", ex);
SQLStateSQLExceptionTranslatorTests.assertTranslation(dae, ex, dataAccessExceptionType);
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.jdbc.support;
import java.sql.BatchUpdateException;
import java.sql.SQLException;
import org.jspecify.annotations.Nullable;
@ -45,6 +46,7 @@ class SQLStateSQLExceptionTranslatorTests {
private final SQLExceptionTranslator translator = new SQLStateSQLExceptionTranslator();
@Test
void translateNullException() {
assertThatIllegalArgumentException().isThrownBy(() -> translator.translate("", "", null));
@ -125,6 +127,16 @@ class SQLStateSQLExceptionTranslatorTests {
assertTranslation("57014", QueryTimeoutException.class);
}
@Test
void translateWithinQualifiedBatch() {
assertTranslation(buildBatchUpdateException("JZ", new SQLException("", "23505", 0)), DuplicateKeyException.class);
}
@Test
void translateWithinUnqualifiedBatch() {
assertTranslation(buildBatchUpdateException(null, new SQLException("", "23505", 0)), DuplicateKeyException.class);
}
@Test
void translateUncategorized() {
assertTranslation("00000000", null);
@ -142,28 +154,41 @@ class SQLStateSQLExceptionTranslatorTests {
*/
@Test
void malformedSqlStateCodes() {
assertTranslation(null, null);
assertTranslation((String) null, null);
assertTranslation("", null);
assertTranslation("I", null);
}
private void assertTranslation(@Nullable String sqlState, @Nullable Class<?> dataAccessExceptionType) {
assertTranslation(sqlState, 0, dataAccessExceptionType);
assertTranslation(new SQLException("reason", sqlState, 0), dataAccessExceptionType);
}
private void assertTranslation(@Nullable String sqlState, int errorCode, @Nullable Class<?> dataAccessExceptionType) {
SQLException ex = new SQLException("reason", sqlState, errorCode);
DataAccessException dax = translator.translate("task", "SQL", ex);
if (dataAccessExceptionType == null) {
assertThat(dax).as("Expected translation to null").isNull();
return;
assertTranslation(new SQLException("reason", sqlState, errorCode), dataAccessExceptionType);
}
assertThat(dax).as("Specific translation must not result in null").isNotNull();
assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex);
private void assertTranslation(SQLException ex, @Nullable Class<?> dataAccessExceptionType) {
DataAccessException dae = translator.translate("task", "SQL", ex);
if (dataAccessExceptionType == null) {
assertThat(dae).as("Expected translation to null").isNull();
return;
}
assertTranslation(dae, ex, dataAccessExceptionType);
}
static void assertTranslation(DataAccessException dae, SQLException ex, Class<?> dataAccessExceptionType) {
assertThat(dae).as("Specific translation must not result in null").isNotNull();
assertThat(dae).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
assertThat(dae.getCause()).as("The exact same original SQLException must be preserved").isSameAs(
ex instanceof BatchUpdateException bue ? bue.getNextException() : ex);
}
static BatchUpdateException buildBatchUpdateException(@Nullable String sqlState, SQLException next) {
BatchUpdateException ex = new BatchUpdateException("", sqlState, null);
ex.setNextException(next);
return ex;
}
}