Skip to content

Commit

Permalink
Detect SQL state 23505/40001 as DuplicateKeyException/CannotAcquireLo…
Browse files Browse the repository at this point in the history
…ckException

Favors PessimisticLockingFailureException over plain ConcurrencyFailureException.
Deprecates CannotSerializeTransactionException and DeadlockLoserDataAccessException.

Closes gh-29511
Closes gh-29675
  • Loading branch information
jhoeller committed Dec 13, 2022
1 parent c79ae0c commit 4c69892
Show file tree
Hide file tree
Showing 18 changed files with 208 additions and 297 deletions.
Expand Up @@ -177,6 +177,7 @@ public SQLErrorCodes getSqlErrorCodes() {
}


@SuppressWarnings("deprecation")
@Override
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
Expand Down
Expand Up @@ -30,12 +30,14 @@
import java.sql.SQLTransientConnectionException;
import java.sql.SQLTransientException;

import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.RecoverableDataAccessException;
import org.springframework.dao.TransientDataAccessResourceException;
Expand Down Expand Up @@ -69,30 +71,36 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
if (ex instanceof SQLTransientConnectionException) {
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLTransactionRollbackException) {
return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex);
if (ex instanceof SQLTransactionRollbackException) {
if ("40001".equals(ex.getSQLState())) {
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
}
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLTimeoutException) {
if (ex instanceof SQLTimeoutException) {
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
}
}
else if (ex instanceof SQLNonTransientException) {
if (ex instanceof SQLNonTransientConnectionException) {
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLDataException) {
if (ex instanceof SQLDataException) {
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLIntegrityConstraintViolationException) {
if (ex instanceof SQLIntegrityConstraintViolationException) {
if ("23505".equals(ex.getSQLState())) {
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
}
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLInvalidAuthorizationSpecException) {
if (ex instanceof SQLInvalidAuthorizationSpecException) {
return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLSyntaxErrorException) {
if (ex instanceof SQLSyntaxErrorException) {
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
}
else if (ex instanceof SQLFeatureNotSupportedException) {
if (ex instanceof SQLFeatureNotSupportedException) {
return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex);
}
}
Expand Down
Expand Up @@ -19,10 +19,12 @@
import java.sql.SQLException;
import java.util.Set;

import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.jdbc.BadSqlGrammarException;
Expand Down Expand Up @@ -77,7 +79,7 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException
"S1" // DB2: communication failure
);

private static final Set<String> CONCURRENCY_FAILURE_CODES = Set.of(
private static final Set<String> PESSIMISTIC_LOCKING_FAILURE_CODES = Set.of(
"40", // Transaction rollback
"61" // Oracle: deadlock
);
Expand All @@ -97,6 +99,9 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
}
else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {
if ("23505".equals(sqlState)) {
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
}
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
}
else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
Expand All @@ -105,8 +110,11 @@ else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
}
else if (CONCURRENCY_FAILURE_CODES.contains(classCode)) {
return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex);
else if (PESSIMISTIC_LOCKING_FAILURE_CODES.contains(classCode)) {
if ("40001".equals(sqlState)) {
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
}
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
}
}

Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
Expand All @@ -16,6 +16,7 @@

package org.springframework.jdbc.support;

import java.sql.SQLDataException;
import java.sql.SQLException;

import org.junit.jupiter.api.Test;
Expand All @@ -37,8 +38,8 @@ public class SQLExceptionCustomTranslatorTests {
private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes();

static {
ERROR_CODES.setBadSqlGrammarCodes(new String[] { "1" });
ERROR_CODES.setDataAccessResourceFailureCodes(new String[] { "2" });
ERROR_CODES.setBadSqlGrammarCodes("1");
ERROR_CODES.setDataAccessResourceFailureCodes("2");
ERROR_CODES.setCustomSqlExceptionTranslatorClass(CustomSqlExceptionTranslator.class);
}

Expand All @@ -47,15 +48,15 @@ public class SQLExceptionCustomTranslatorTests {

@Test
public void badSqlGrammarException() {
SQLException badSqlGrammarExceptionEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 1);
SQLException badSqlGrammarExceptionEx = new SQLDataException("", "", 1);
DataAccessException dae = sext.translate("task", "SQL", badSqlGrammarExceptionEx);
assertThat(dae.getCause()).isEqualTo(badSqlGrammarExceptionEx);
assertThat(dae).isInstanceOf(BadSqlGrammarException.class);
}

@Test
public void dataAccessResourceException() {
SQLException dataAccessResourceEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 2);
SQLException dataAccessResourceEx = new SQLDataException("", "", 2);
DataAccessException dae = sext.translate("task", "SQL", dataAccessResourceEx);
assertThat(dae.getCause()).isEqualTo(dataAccessResourceEx);
assertThat(dae).isInstanceOf(TransientDataAccessResourceException.class);
Expand Down

This file was deleted.

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
Expand All @@ -16,15 +16,28 @@

package org.springframework.jdbc.support;

import java.sql.SQLDataException;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.sql.SQLInvalidAuthorizationSpecException;
import java.sql.SQLNonTransientConnectionException;
import java.sql.SQLRecoverableException;
import java.sql.SQLSyntaxErrorException;
import java.sql.SQLTimeoutException;
import java.sql.SQLTransactionRollbackException;
import java.sql.SQLTransientConnectionException;

import org.junit.jupiter.api.Test;

import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.RecoverableDataAccessException;
import org.springframework.dao.TransientDataAccessResourceException;
Expand All @@ -34,78 +47,43 @@

/**
* @author Thomas Risberg
* @author Juergen Hoeller
*/
public class SQLExceptionSubclassTranslatorTests {

private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes();

static {
ERROR_CODES.setBadSqlGrammarCodes("1");
@Test
public 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 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);
}


@Test
public void errorCodeTranslation() {
SQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(ERROR_CODES);

SQLException dataIntegrityViolationEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 0);
DataIntegrityViolationException divex = (DataIntegrityViolationException) sext.translate("task", "SQL", dataIntegrityViolationEx);
assertThat(divex.getCause()).isEqualTo(dataIntegrityViolationEx);

SQLException featureNotSupEx = SQLExceptionSubclassFactory.newSQLFeatureNotSupportedException("", "", 0);
InvalidDataAccessApiUsageException idaex = (InvalidDataAccessApiUsageException) sext.translate("task", "SQL", featureNotSupEx);
assertThat(idaex.getCause()).isEqualTo(featureNotSupEx);

SQLException dataIntegrityViolationEx2 = SQLExceptionSubclassFactory.newSQLIntegrityConstraintViolationException("", "", 0);
DataIntegrityViolationException divex2 = (DataIntegrityViolationException) sext.translate("task", "SQL", dataIntegrityViolationEx2);
assertThat(divex2.getCause()).isEqualTo(dataIntegrityViolationEx2);

SQLException permissionDeniedEx = SQLExceptionSubclassFactory.newSQLInvalidAuthorizationSpecException("", "", 0);
PermissionDeniedDataAccessException pdaex = (PermissionDeniedDataAccessException) sext.translate("task", "SQL", permissionDeniedEx);
assertThat(pdaex.getCause()).isEqualTo(permissionDeniedEx);

SQLException dataAccessResourceEx = SQLExceptionSubclassFactory.newSQLNonTransientConnectionException("", "", 0);
DataAccessResourceFailureException darex = (DataAccessResourceFailureException) sext.translate("task", "SQL", dataAccessResourceEx);
assertThat(darex.getCause()).isEqualTo(dataAccessResourceEx);

SQLException badSqlEx2 = SQLExceptionSubclassFactory.newSQLSyntaxErrorException("", "", 0);
BadSqlGrammarException bsgex2 = (BadSqlGrammarException) sext.translate("task", "SQL2", badSqlEx2);
assertThat(bsgex2.getSql()).isEqualTo("SQL2");
assertThat((Object) bsgex2.getSQLException()).isEqualTo(badSqlEx2);

SQLException tranRollbackEx = SQLExceptionSubclassFactory.newSQLTransactionRollbackException("", "", 0);
ConcurrencyFailureException cfex = (ConcurrencyFailureException) sext.translate("task", "SQL", tranRollbackEx);
assertThat(cfex.getCause()).isEqualTo(tranRollbackEx);

SQLException transientConnEx = SQLExceptionSubclassFactory.newSQLTransientConnectionException("", "", 0);
TransientDataAccessResourceException tdarex = (TransientDataAccessResourceException) sext.translate("task", "SQL", transientConnEx);
assertThat(tdarex.getCause()).isEqualTo(transientConnEx);

SQLException transientConnEx2 = SQLExceptionSubclassFactory.newSQLTimeoutException("", "", 0);
QueryTimeoutException tdarex2 = (QueryTimeoutException) sext.translate("task", "SQL", transientConnEx2);
assertThat(tdarex2.getCause()).isEqualTo(transientConnEx2);

SQLException recoverableEx = SQLExceptionSubclassFactory.newSQLRecoverableException("", "", 0);
RecoverableDataAccessException rdaex2 = (RecoverableDataAccessException) sext.translate("task", "SQL", recoverableEx);
assertThat(rdaex2.getCause()).isEqualTo(recoverableEx);

// Test classic error code translation. We should move there next if the exception we pass in is not one
// of the new subclasses.
SQLException sexEct = new SQLException("", "", 1);
BadSqlGrammarException bsgEct = (BadSqlGrammarException) sext.translate("task", "SQL-ECT", sexEct);
assertThat(bsgEct.getSql()).isEqualTo("SQL-ECT");
assertThat((Object) bsgEct.getSQLException()).isEqualTo(sexEct);

public 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
SQLException sexFbt = new SQLException("", "07xxx", 666666666);
BadSqlGrammarException bsgFbt = (BadSqlGrammarException) sext.translate("task", "SQL-FBT", sexFbt);
assertThat(bsgFbt.getSql()).isEqualTo("SQL-FBT");
assertThat((Object) bsgFbt.getSQLException()).isEqualTo(sexFbt);
doTest(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class);
// and 08xxx will be data resource failure (non-transient) picked up by the fallback SQLState translator
SQLException sexFbt2 = new SQLException("", "08xxx", 666666666);
DataAccessResourceFailureException darfFbt = (DataAccessResourceFailureException) sext.translate("task", "SQL-FBT2", sexFbt2);
assertThat(darfFbt.getCause()).isEqualTo(sexFbt2);
doTest(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);
}

}

0 comments on commit 4c69892

Please sign in to comment.