Browse Source

Detect SQL state 23505/40001 as DuplicateKeyException/CannotAcquireLockException

Favors PessimisticLockingFailureException over plain ConcurrencyFailureException.
Deprecates CannotSerializeTransactionException and DeadlockLoserDataAccessException.

Closes gh-29511
Closes gh-29675
pull/29692/head
Juergen Hoeller 3 years ago
parent
commit
4c69892f39
  1. 1
      spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java
  2. 26
      spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java
  3. 16
      spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java
  4. 11
      spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java
  5. 78
      spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java
  6. 108
      spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java
  7. 77
      spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java
  8. 78
      spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java
  9. 16
      spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java
  10. 35
      spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java
  11. 5
      spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java
  12. 8
      spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java
  13. 4
      spring-tx/src/main/java/org/springframework/dao/CleanupFailureDataAccessException.java
  14. 10
      spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java
  15. 9
      spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java
  16. 8
      spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java
  17. 5
      spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java
  18. 10
      spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java

1
spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java

@ -177,6 +177,7 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep @@ -177,6 +177,7 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep
}
@SuppressWarnings("deprecation")
@Override
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {

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

@ -30,12 +30,14 @@ import java.sql.SQLTransactionRollbackException; @@ -30,12 +30,14 @@ import java.sql.SQLTransactionRollbackException;
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;
@ -69,10 +71,13 @@ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLException @@ -69,10 +71,13 @@ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLException
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);
}
}
@ -80,19 +85,22 @@ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLException @@ -80,19 +85,22 @@ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLException
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);
}
}

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

@ -19,10 +19,12 @@ package org.springframework.jdbc.support; @@ -19,10 +19,12 @@ package org.springframework.jdbc.support;
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;
@ -77,7 +79,7 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException @@ -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
);
@ -97,6 +99,9 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException @@ -97,6 +99,9 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException
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)) {
@ -105,8 +110,11 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException @@ -105,8 +110,11 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException
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);
}
}

11
spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java

@ -1,5 +1,5 @@ @@ -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.
@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package org.springframework.jdbc.support;
import java.sql.SQLDataException;
import java.sql.SQLException;
import org.junit.jupiter.api.Test;
@ -37,8 +38,8 @@ public class SQLExceptionCustomTranslatorTests { @@ -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);
}
@ -47,7 +48,7 @@ public class SQLExceptionCustomTranslatorTests { @@ -47,7 +48,7 @@ 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);
@ -55,7 +56,7 @@ public class SQLExceptionCustomTranslatorTests { @@ -55,7 +56,7 @@ public class SQLExceptionCustomTranslatorTests {
@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);

78
spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java

@ -1,78 +0,0 @@ @@ -1,78 +0,0 @@
/*
* 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.
* 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.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;
/**
* Class to generate {@link SQLException} subclasses for testing purposes.
*
* @author Thomas Risberg
*/
public class SQLExceptionSubclassFactory {
public static SQLException newSQLDataException(String reason, String SQLState, int vendorCode) {
return new SQLDataException(reason, SQLState, vendorCode);
}
public static SQLException newSQLFeatureNotSupportedException(String reason, String SQLState, int vendorCode) {
return new SQLFeatureNotSupportedException(reason, SQLState, vendorCode);
}
public static SQLException newSQLIntegrityConstraintViolationException(String reason, String SQLState, int vendorCode) {
return new SQLIntegrityConstraintViolationException(reason, SQLState, vendorCode);
}
public static SQLException newSQLInvalidAuthorizationSpecException(String reason, String SQLState, int vendorCode) {
return new SQLInvalidAuthorizationSpecException(reason, SQLState, vendorCode);
}
public static SQLException newSQLNonTransientConnectionException(String reason, String SQLState, int vendorCode) {
return new SQLNonTransientConnectionException(reason, SQLState, vendorCode);
}
public static SQLException newSQLSyntaxErrorException(String reason, String SQLState, int vendorCode) {
return new SQLSyntaxErrorException(reason, SQLState, vendorCode);
}
public static SQLException newSQLTransactionRollbackException(String reason, String SQLState, int vendorCode) {
return new SQLTransactionRollbackException(reason, SQLState, vendorCode);
}
public static SQLException newSQLTransientConnectionException(String reason, String SQLState, int vendorCode) {
return new SQLTransientConnectionException(reason, SQLState, vendorCode);
}
public static SQLException newSQLTimeoutException(String reason, String SQLState, int vendorCode) {
return new SQLTimeoutException(reason, SQLState, vendorCode);
}
public static SQLException newSQLRecoverableException(String reason, String SQLState, int vendorCode) {
return new SQLRecoverableException(reason, SQLState, vendorCode);
}
}

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

@ -1,5 +1,5 @@ @@ -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.
@ -16,15 +16,28 @@ @@ -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;
@ -34,78 +47,43 @@ import static org.assertj.core.api.Assertions.assertThat; @@ -34,78 +47,43 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @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);
}
}

77
spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java

@ -1,77 +0,0 @@ @@ -1,77 +0,0 @@
/*
* Copyright 2002-2019 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.support;
import java.sql.SQLException;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.BadSqlGrammarException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rod Johnson
* @since 13-Jan-03
*/
public class SQLStateExceptionTranslatorTests {
private static final String sql = "SELECT FOO FROM BAR";
private final SQLStateSQLExceptionTranslator trans = new SQLStateSQLExceptionTranslator();
// ALSO CHECK CHAIN of SQLExceptions!?
// also allow chain of translators? default if can't do specific?
@Test
public void badSqlGrammar() {
SQLException sex = new SQLException("Message", "42001", 1);
try {
throw this.trans.translate("task", sql, sex);
}
catch (BadSqlGrammarException ex) {
// OK
assertThat(sql.equals(ex.getSql())).as("SQL is correct").isTrue();
assertThat(sex.equals(ex.getSQLException())).as("Exception matches").isTrue();
}
}
@Test
public void invalidSqlStateCode() {
SQLException sex = new SQLException("Message", "NO SUCH CODE", 1);
assertThat(this.trans.translate("task", sql, sex)).isNull();
}
/**
* PostgreSQL can return null.
* SAP DB can apparently return empty SQL code.
* Bug 729170
*/
@Test
public void malformedSqlStateCodes() {
SQLException sex = new SQLException("Message", null, 1);
assertThat(this.trans.translate("task", sql, sex)).isNull();
sex = new SQLException("Message", "", 1);
assertThat(this.trans.translate("task", sql, sex)).isNull();
// One char's not allowed
sex = new SQLException("Message", "I", 1);
assertThat(this.trans.translate("task", sql, sex)).isNull();
}
}

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@ -20,12 +20,15 @@ import java.sql.SQLException; @@ -20,12 +20,15 @@ import java.sql.SQLException;
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.PessimisticLockingFailureException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -37,58 +40,83 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException @@ -37,58 +40,83 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/
public class SQLStateSQLExceptionTranslatorTests {
private static final String REASON = "The game is afoot!";
private static final String TASK = "Counting sheep... yawn.";
private static final String SQL = "select count(0) from t_sheep where over_fence = ... yawn... 1";
@Test
public void testTranslateNullException() {
public void translateNullException() {
assertThatIllegalArgumentException().isThrownBy(() ->
new SQLStateSQLExceptionTranslator().translate("", "", null));
}
@Test
public void testTranslateBadSqlGrammar() {
public void translateBadSqlGrammar() {
doTest("07", BadSqlGrammarException.class);
}
@Test
public void testTranslateDataIntegrityViolation() {
public void translateDataIntegrityViolation() {
doTest("23", DataIntegrityViolationException.class);
}
@Test
public void testTranslateDataAccessResourceFailure() {
public void translateDuplicateKey() {
doTest("23505", DuplicateKeyException.class);
}
@Test
public void translateDataAccessResourceFailure() {
doTest("53", DataAccessResourceFailureException.class);
}
@Test
public void testTranslateTransientDataAccessResourceFailure() {
public void translateTransientDataAccessResourceFailure() {
doTest("S1", TransientDataAccessResourceException.class);
}
@Test
public void testTranslateConcurrencyFailure() {
doTest("40", ConcurrencyFailureException.class);
public void translatePessimisticLockingFailure() {
doTest("40", PessimisticLockingFailureException.class);
}
@Test
public void translateCannotAcquireLock() {
doTest("40001", CannotAcquireLockException.class);
}
@Test
public void testTranslateUncategorized() {
assertThat(new SQLStateSQLExceptionTranslator().translate("", "", new SQLException(REASON, "00000000"))).isNull();
public void translateUncategorized() {
doTest("00000000", null);
}
@Test
public void invalidSqlStateCode() {
doTest("NO SUCH CODE", null);
}
private void doTest(String sqlState, Class<?> dataAccessExceptionType) {
SQLException ex = new SQLException(REASON, sqlState);
/**
* PostgreSQL can return null.
* SAP DB can apparently return empty SQL code.
* Bug 729170
*/
@Test
public void malformedSqlStateCodes() {
doTest(null, null);
doTest("", null);
doTest("I", null);
}
private void doTest(@Nullable String sqlState, @Nullable Class<?> dataAccessExceptionType) {
SQLExceptionTranslator translator = new SQLStateSQLExceptionTranslator();
DataAccessException dax = translator.translate(TASK, SQL, ex);
assertThat(dax).as("Specific translation must not result in a null DataAccessException being returned.").isNotNull();
assertThat(dax.getClass()).as("Wrong DataAccessException type returned as the result of the translation").isEqualTo(dataAccessExceptionType);
assertThat(dax.getCause()).as("The original SQLException must be preserved in the translated DataAccessException").isNotNull();
assertThat(dax.getCause()).as("The exact same original SQLException must be preserved in the translated DataAccessException").isSameAs(ex);
SQLException ex = new SQLException("reason", sqlState);
DataAccessException dax = translator.translate("task", "SQL", ex);
if (dataAccessExceptionType == null) {
assertThat(dax).as("Expected translation to null").isNull();
return;
}
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);
}
}

16
spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@ -32,11 +32,13 @@ import io.r2dbc.spi.Wrapped; @@ -32,11 +32,13 @@ import io.r2dbc.spi.Wrapped;
import reactor.core.publisher.Mono;
import org.springframework.core.Ordered;
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.PermissionDeniedDataAccessException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.lang.Nullable;
@ -215,17 +217,23 @@ public abstract class ConnectionFactoryUtils { @@ -215,17 +217,23 @@ public abstract class ConnectionFactoryUtils {
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
}
if (ex instanceof R2dbcRollbackException) {
return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex);
if ("40001".equals(ex.getSqlState())) {
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
}
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
}
if (ex instanceof R2dbcTimeoutException) {
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
}
}
if (ex instanceof R2dbcNonTransientException) {
else if (ex instanceof R2dbcNonTransientException) {
if (ex instanceof R2dbcNonTransientResourceException) {
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
}
if (ex instanceof R2dbcDataIntegrityViolationException) {
if ("23505".equals(ex.getSqlState())) {
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
}
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
}
if (ex instanceof R2dbcPermissionDeniedException) {

35
spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java

@ -26,10 +26,12 @@ import io.r2dbc.spi.R2dbcTimeoutException; @@ -26,10 +26,12 @@ import io.r2dbc.spi.R2dbcTimeoutException;
import io.r2dbc.spi.R2dbcTransientResourceException;
import org.junit.jupiter.api.Test;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.r2dbc.BadSqlGrammarException;
@ -41,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat; @@ -41,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Unit tests for {@link ConnectionFactoryUtils}.
*
* @author Mark Paluch
* @author Juergen Hoeller
*/
public class ConnectionFactoryUtilsUnitTests {
@ -48,63 +51,71 @@ public class ConnectionFactoryUtilsUnitTests { @@ -48,63 +51,71 @@ public class ConnectionFactoryUtilsUnitTests {
public void shouldTranslateTransientResourceException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcTransientResourceException(""));
assertThat(exception).isInstanceOf(TransientDataAccessResourceException.class);
assertThat(exception).isExactlyInstanceOf(TransientDataAccessResourceException.class);
}
@Test
public void shouldTranslateRollbackException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcRollbackException());
assertThat(exception).isInstanceOf(ConcurrencyFailureException.class);
assertThat(exception).isExactlyInstanceOf(PessimisticLockingFailureException.class);
exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcRollbackException("reason", "40001"));
assertThat(exception).isExactlyInstanceOf(CannotAcquireLockException.class);
}
@Test
public void shouldTranslateTimeoutException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcTimeoutException());
assertThat(exception).isInstanceOf(QueryTimeoutException.class);
assertThat(exception).isExactlyInstanceOf(QueryTimeoutException.class);
}
@Test
public void shouldNotTranslateUnknownExceptions() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new MyTransientExceptions());
assertThat(exception).isInstanceOf(UncategorizedR2dbcException.class);
assertThat(exception).isExactlyInstanceOf(UncategorizedR2dbcException.class);
}
@Test
public void shouldTranslateNonTransientResourceException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcNonTransientResourceException());
assertThat(exception).isInstanceOf(DataAccessResourceFailureException.class);
assertThat(exception).isExactlyInstanceOf(DataAccessResourceFailureException.class);
}
@Test
public void shouldTranslateIntegrityViolationException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException());
assertThat(exception).isInstanceOf(DataIntegrityViolationException.class);
assertThat(exception).isExactlyInstanceOf(DataIntegrityViolationException.class);
exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException("reason", "23505"));
assertThat(exception).isExactlyInstanceOf(DuplicateKeyException.class);
}
@Test
public void shouldTranslatePermissionDeniedException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcPermissionDeniedException());
assertThat(exception).isInstanceOf(PermissionDeniedDataAccessException.class);
assertThat(exception).isExactlyInstanceOf(PermissionDeniedDataAccessException.class);
}
@Test
public void shouldTranslateBadSqlGrammarException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcBadGrammarException());
assertThat(exception).isInstanceOf(BadSqlGrammarException.class);
assertThat(exception).isExactlyInstanceOf(BadSqlGrammarException.class);
}
@Test
public void messageGeneration() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK",
"SOME-SQL", new R2dbcTransientResourceException("MESSAGE"));
assertThat(exception).isInstanceOf(
assertThat(exception).isExactlyInstanceOf(
TransientDataAccessResourceException.class).hasMessage("TASK; SQL [SOME-SQL]; MESSAGE");
}
@ -112,7 +123,7 @@ public class ConnectionFactoryUtilsUnitTests { @@ -112,7 +123,7 @@ public class ConnectionFactoryUtilsUnitTests {
public void messageGenerationNullSQL() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK", null,
new R2dbcTransientResourceException("MESSAGE"));
assertThat(exception).isInstanceOf(
assertThat(exception).isExactlyInstanceOf(
TransientDataAccessResourceException.class).hasMessage("TASK; MESSAGE");
}
@ -120,7 +131,7 @@ public class ConnectionFactoryUtilsUnitTests { @@ -120,7 +131,7 @@ public class ConnectionFactoryUtilsUnitTests {
public void messageGenerationNullMessage() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK",
"SOME-SQL", new R2dbcTransientResourceException());
assertThat(exception).isInstanceOf(
assertThat(exception).isExactlyInstanceOf(
TransientDataAccessResourceException.class).hasMessage("TASK; SQL [SOME-SQL]; null");
}

5
spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@ -20,6 +20,9 @@ package org.springframework.dao; @@ -20,6 +20,9 @@ package org.springframework.dao;
* Exception thrown on failure to acquire a lock during an update,
* for example during a "select for update" statement.
*
* <p>Consider handling the general {@link PessimisticLockingFailureException}
* instead, semantically including a wider range of locking-related failures.
*
* @author Rod Johnson
*/
@SuppressWarnings("serial")

8
spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@ -20,8 +20,14 @@ package org.springframework.dao; @@ -20,8 +20,14 @@ package org.springframework.dao;
* Exception thrown on failure to complete a transaction in serialized mode
* due to update conflicts.
*
* <p>Consider handling the general {@link PessimisticLockingFailureException}
* instead, semantically including a wider range of locking-related failures.
*
* @author Rod Johnson
* @deprecated as of 6.0.3, in favor of
* {@link PessimisticLockingFailureException}/{@link CannotAcquireLockException}
*/
@Deprecated(since = "6.0.3")
@SuppressWarnings("serial")
public class CannotSerializeTransactionException extends PessimisticLockingFailureException {

4
spring-tx/src/main/java/org/springframework/dao/CleanupFailureDataAccessException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@ -28,7 +28,9 @@ package org.springframework.dao; @@ -28,7 +28,9 @@ package org.springframework.dao;
* to keep the original data access exception, if any.
*
* @author Rod Johnson
* @deprecated as of 6.0.3 since it is not in use within core JDBC/ORM support
*/
@Deprecated(since = "6.0.3")
@SuppressWarnings("serial")
public class CleanupFailureDataAccessException extends NonTransientDataAccessException {

10
spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 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.
@ -19,17 +19,15 @@ package org.springframework.dao; @@ -19,17 +19,15 @@ package org.springframework.dao;
import org.springframework.lang.Nullable;
/**
* Exception thrown on concurrency failure.
* Exception thrown on various data access concurrency failures.
*
* <p>This exception should be subclassed to indicate the type of failure:
* optimistic locking, failure to acquire lock, etc.
* <p>This exception provides subclasses for specific types of failure,
* in particular optimistic locking versus pessimistic locking.
*
* @author Thomas Risberg
* @since 1.1
* @see OptimisticLockingFailureException
* @see PessimisticLockingFailureException
* @see CannotAcquireLockException
* @see DeadlockLoserDataAccessException
*/
@SuppressWarnings("serial")
public class ConcurrencyFailureException extends TransientDataAccessException {

9
spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java

@ -19,8 +19,13 @@ package org.springframework.dao; @@ -19,8 +19,13 @@ package org.springframework.dao;
/**
* Exception thrown when an attempt to insert or update data
* results in violation of an integrity constraint. Note that this
* is not purely a relational concept; unique primary keys are
* required by most database types.
* is not purely a relational concept; integrity constraints such
* as unique primary keys are required by most database types.
*
* <p>Serves as a superclass for more specific exceptions, e.g.
* {@link DuplicateKeyException}. However, it is generally
* recommended to handle {@code DataIntegrityViolationException}
* itself instead of relying on specific exception subclasses.
*
* @author Rod Johnson
*/

8
spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@ -20,8 +20,14 @@ package org.springframework.dao; @@ -20,8 +20,14 @@ package org.springframework.dao;
* Generic exception thrown when the current process was
* a deadlock loser, and its transaction rolled back.
*
* <p>Consider handling the general {@link PessimisticLockingFailureException}
* instead, semantically including a wider range of locking-related failures.
*
* @author Rod Johnson
* @deprecated as of 6.0.3, in favor of
* {@link PessimisticLockingFailureException}/{@link CannotAcquireLockException}
*/
@Deprecated(since = "6.0.3")
@SuppressWarnings("serial")
public class DeadlockLoserDataAccessException extends PessimisticLockingFailureException {

5
spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@ -22,6 +22,9 @@ package org.springframework.dao; @@ -22,6 +22,9 @@ package org.springframework.dao;
* Note that this is not necessarily a purely relational concept;
* unique primary keys are required by most database types.
*
* <p>Consider handling the general {@link DataIntegrityViolationException}
* instead, semantically including a wider range of constraint violations.
*
* @author Thomas Risberg
*/
@SuppressWarnings("serial")

10
spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@ -21,13 +21,13 @@ package org.springframework.dao; @@ -21,13 +21,13 @@ package org.springframework.dao;
* Thrown by Spring's SQLException translation mechanism
* if a corresponding database error is encountered.
*
* <p>Serves as superclass for more specific exceptions, like
* CannotAcquireLockException and DeadlockLoserDataAccessException.
* <p>Serves as a superclass for more specific exceptions, e.g.
* {@link CannotAcquireLockException}. However, it is generally
* recommended to handle {@code PessimisticLockingFailureException}
* itself instead of relying on specific exception subclasses.
*
* @author Thomas Risberg
* @since 1.2
* @see CannotAcquireLockException
* @see DeadlockLoserDataAccessException
* @see OptimisticLockingFailureException
*/
@SuppressWarnings("serial")

Loading…
Cancel
Save