diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java index 8be60a7259ce..a0622bfdcafa 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java @@ -177,6 +177,7 @@ public SQLErrorCodes getSqlErrorCodes() { } + @SuppressWarnings("deprecation") @Override @Nullable protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java index 7e86c84fce43..56f227bf1f57 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java @@ -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; @@ -69,10 +71,13 @@ 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); } } @@ -80,19 +85,22 @@ 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); } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java index 9bdec521d598..897a8321e122 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java @@ -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; @@ -77,7 +79,7 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException "S1" // DB2: communication failure ); - private static final Set CONCURRENCY_FAILURE_CODES = Set.of( + private static final Set PESSIMISTIC_LOCKING_FAILURE_CODES = Set.of( "40", // Transaction rollback "61" // Oracle: deadlock ); @@ -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)) { @@ -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); } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java index 8d46c0f578d4..0e1228df7821 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java @@ -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 @@ 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 { 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 { @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 void badSqlGrammarException() { @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); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java deleted file mode 100644 index a172139de1e5..000000000000 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java +++ /dev/null @@ -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); - } - -} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java index 0be4d4b0d8d4..fc403e265a88 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java @@ -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 @@ 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 @@ /** * @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); } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java deleted file mode 100644 index 608e0b5d3312..000000000000 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java +++ /dev/null @@ -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(); - } - -} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java index 98baf1ab7f9d..b667d3fae2ec 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java @@ -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 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 @@ */ 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); } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java index dc3fab49d326..83dde60db7b1 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java @@ -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 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 static DataAccessException convertR2dbcException(String task, @Nullable S 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) { diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java index ebbf3c370b87..f8449ab7782a 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java @@ -26,10 +26,12 @@ 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 @@ * Unit tests for {@link ConnectionFactoryUtils}. * * @author Mark Paluch + * @author Juergen Hoeller */ 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 void messageGeneration() { 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 void messageGenerationNullSQL() { 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"); } diff --git a/spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java b/spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java index d5b113e9f5e2..5a36c2381545 100644 --- a/spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java +++ b/spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java @@ -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 @@ * Exception thrown on failure to acquire a lock during an update, * for example during a "select for update" statement. * + *

Consider handling the general {@link PessimisticLockingFailureException} + * instead, semantically including a wider range of locking-related failures. + * * @author Rod Johnson */ @SuppressWarnings("serial") diff --git a/spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java b/spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java index 1dfe7f541674..6968981c05a6 100644 --- a/spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java +++ b/spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java @@ -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 @@ * Exception thrown on failure to complete a transaction in serialized mode * due to update conflicts. * + *

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 { diff --git a/spring-tx/src/main/java/org/springframework/dao/CleanupFailureDataAccessException.java b/spring-tx/src/main/java/org/springframework/dao/CleanupFailureDataAccessException.java index 32dfd248f479..525a771f53ee 100644 --- a/spring-tx/src/main/java/org/springframework/dao/CleanupFailureDataAccessException.java +++ b/spring-tx/src/main/java/org/springframework/dao/CleanupFailureDataAccessException.java @@ -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 @@ * 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 { diff --git a/spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java b/spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java index cc990803f483..4f1031ce3f14 100644 --- a/spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java +++ b/spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java @@ -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 @@ import org.springframework.lang.Nullable; /** - * Exception thrown on concurrency failure. + * Exception thrown on various data access concurrency failures. * - *

This exception should be subclassed to indicate the type of failure: - * optimistic locking, failure to acquire lock, etc. + *

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 { diff --git a/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java b/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java index b1347e1ee946..ad1cc68bb972 100644 --- a/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java +++ b/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java @@ -19,8 +19,13 @@ /** * 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. + * + *

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 */ diff --git a/spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java b/spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java index 3c946cd56408..0f41fa3505da 100644 --- a/spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java +++ b/spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java @@ -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 @@ * Generic exception thrown when the current process was * a deadlock loser, and its transaction rolled back. * + *

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 { diff --git a/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java b/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java index b406391a3ff3..36ee084c9e7b 100644 --- a/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java +++ b/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java @@ -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 @@ * Note that this is not necessarily a purely relational concept; * unique primary keys are required by most database types. * + *

Consider handling the general {@link DataIntegrityViolationException} + * instead, semantically including a wider range of constraint violations. + * * @author Thomas Risberg */ @SuppressWarnings("serial") diff --git a/spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java b/spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java index 3e60eb141bca..31599203a997 100644 --- a/spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java +++ b/spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java @@ -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 @@ * Thrown by Spring's SQLException translation mechanism * if a corresponding database error is encountered. * - *

Serves as superclass for more specific exceptions, like - * CannotAcquireLockException and DeadlockLoserDataAccessException. + *

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")