Merge branch '6.2.x'
# 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:
commit
54b43d4a88
|
@ -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...
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
assertTranslation(new SQLException("reason", sqlState, errorCode), dataAccessExceptionType);
|
||||
}
|
||||
|
||||
private void assertTranslation(SQLException ex, @Nullable Class<?> dataAccessExceptionType) {
|
||||
DataAccessException dae = translator.translate("task", "SQL", ex);
|
||||
|
||||
if (dataAccessExceptionType == null) {
|
||||
assertThat(dax).as("Expected translation to null").isNull();
|
||||
assertThat(dae).as("Expected translation to null").isNull();
|
||||
return;
|
||||
}
|
||||
assertTranslation(dae, ex, 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);
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue