Skip to content

Commit 4c69892

Browse files
committed
Detect SQL state 23505/40001 as DuplicateKeyException/CannotAcquireLockException
Favors PessimisticLockingFailureException over plain ConcurrencyFailureException. Deprecates CannotSerializeTransactionException and DeadlockLoserDataAccessException. Closes gh-29511 Closes gh-29675
1 parent c79ae0c commit 4c69892

18 files changed

+208
-297
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ public SQLErrorCodes getSqlErrorCodes() {
177177
}
178178

179179

180+
@SuppressWarnings("deprecation")
180181
@Override
181182
@Nullable
182183
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030
import java.sql.SQLTransientConnectionException;
3131
import java.sql.SQLTransientException;
3232

33-
import org.springframework.dao.ConcurrencyFailureException;
33+
import org.springframework.dao.CannotAcquireLockException;
3434
import org.springframework.dao.DataAccessException;
3535
import org.springframework.dao.DataAccessResourceFailureException;
3636
import org.springframework.dao.DataIntegrityViolationException;
37+
import org.springframework.dao.DuplicateKeyException;
3738
import org.springframework.dao.InvalidDataAccessApiUsageException;
3839
import org.springframework.dao.PermissionDeniedDataAccessException;
40+
import org.springframework.dao.PessimisticLockingFailureException;
3941
import org.springframework.dao.QueryTimeoutException;
4042
import org.springframework.dao.RecoverableDataAccessException;
4143
import org.springframework.dao.TransientDataAccessResourceException;
@@ -69,30 +71,36 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
6971
if (ex instanceof SQLTransientConnectionException) {
7072
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
7173
}
72-
else if (ex instanceof SQLTransactionRollbackException) {
73-
return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex);
74+
if (ex instanceof SQLTransactionRollbackException) {
75+
if ("40001".equals(ex.getSQLState())) {
76+
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
77+
}
78+
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
7479
}
75-
else if (ex instanceof SQLTimeoutException) {
80+
if (ex instanceof SQLTimeoutException) {
7681
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
7782
}
7883
}
7984
else if (ex instanceof SQLNonTransientException) {
8085
if (ex instanceof SQLNonTransientConnectionException) {
8186
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
8287
}
83-
else if (ex instanceof SQLDataException) {
88+
if (ex instanceof SQLDataException) {
8489
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
8590
}
86-
else if (ex instanceof SQLIntegrityConstraintViolationException) {
91+
if (ex instanceof SQLIntegrityConstraintViolationException) {
92+
if ("23505".equals(ex.getSQLState())) {
93+
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
94+
}
8795
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
8896
}
89-
else if (ex instanceof SQLInvalidAuthorizationSpecException) {
97+
if (ex instanceof SQLInvalidAuthorizationSpecException) {
9098
return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex);
9199
}
92-
else if (ex instanceof SQLSyntaxErrorException) {
100+
if (ex instanceof SQLSyntaxErrorException) {
93101
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
94102
}
95-
else if (ex instanceof SQLFeatureNotSupportedException) {
103+
if (ex instanceof SQLFeatureNotSupportedException) {
96104
return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex);
97105
}
98106
}

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
import java.sql.SQLException;
2020
import java.util.Set;
2121

22-
import org.springframework.dao.ConcurrencyFailureException;
22+
import org.springframework.dao.CannotAcquireLockException;
2323
import org.springframework.dao.DataAccessException;
2424
import org.springframework.dao.DataAccessResourceFailureException;
2525
import org.springframework.dao.DataIntegrityViolationException;
26+
import org.springframework.dao.DuplicateKeyException;
27+
import org.springframework.dao.PessimisticLockingFailureException;
2628
import org.springframework.dao.QueryTimeoutException;
2729
import org.springframework.dao.TransientDataAccessResourceException;
2830
import org.springframework.jdbc.BadSqlGrammarException;
@@ -77,7 +79,7 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException
7779
"S1" // DB2: communication failure
7880
);
7981

80-
private static final Set<String> CONCURRENCY_FAILURE_CODES = Set.of(
82+
private static final Set<String> PESSIMISTIC_LOCKING_FAILURE_CODES = Set.of(
8183
"40", // Transaction rollback
8284
"61" // Oracle: deadlock
8385
);
@@ -97,6 +99,9 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
9799
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
98100
}
99101
else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {
102+
if ("23505".equals(sqlState)) {
103+
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
104+
}
100105
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
101106
}
102107
else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
@@ -105,8 +110,11 @@ else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
105110
else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {
106111
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
107112
}
108-
else if (CONCURRENCY_FAILURE_CODES.contains(classCode)) {
109-
return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex);
113+
else if (PESSIMISTIC_LOCKING_FAILURE_CODES.contains(classCode)) {
114+
if ("40001".equals(sqlState)) {
115+
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
116+
}
117+
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
110118
}
111119
}
112120

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.jdbc.support;
1818

19+
import java.sql.SQLDataException;
1920
import java.sql.SQLException;
2021

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

3940
static {
40-
ERROR_CODES.setBadSqlGrammarCodes(new String[] { "1" });
41-
ERROR_CODES.setDataAccessResourceFailureCodes(new String[] { "2" });
41+
ERROR_CODES.setBadSqlGrammarCodes("1");
42+
ERROR_CODES.setDataAccessResourceFailureCodes("2");
4243
ERROR_CODES.setCustomSqlExceptionTranslatorClass(CustomSqlExceptionTranslator.class);
4344
}
4445

@@ -47,15 +48,15 @@ public class SQLExceptionCustomTranslatorTests {
4748

4849
@Test
4950
public void badSqlGrammarException() {
50-
SQLException badSqlGrammarExceptionEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 1);
51+
SQLException badSqlGrammarExceptionEx = new SQLDataException("", "", 1);
5152
DataAccessException dae = sext.translate("task", "SQL", badSqlGrammarExceptionEx);
5253
assertThat(dae.getCause()).isEqualTo(badSqlGrammarExceptionEx);
5354
assertThat(dae).isInstanceOf(BadSqlGrammarException.class);
5455
}
5556

5657
@Test
5758
public void dataAccessResourceException() {
58-
SQLException dataAccessResourceEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 2);
59+
SQLException dataAccessResourceEx = new SQLDataException("", "", 2);
5960
DataAccessException dae = sext.translate("task", "SQL", dataAccessResourceEx);
6061
assertThat(dae.getCause()).isEqualTo(dataAccessResourceEx);
6162
assertThat(dae).isInstanceOf(TransientDataAccessResourceException.class);

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

Lines changed: 0 additions & 78 deletions
This file was deleted.
Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,15 +16,28 @@
1616

1717
package org.springframework.jdbc.support;
1818

19+
import java.sql.SQLDataException;
1920
import java.sql.SQLException;
21+
import java.sql.SQLFeatureNotSupportedException;
22+
import java.sql.SQLIntegrityConstraintViolationException;
23+
import java.sql.SQLInvalidAuthorizationSpecException;
24+
import java.sql.SQLNonTransientConnectionException;
25+
import java.sql.SQLRecoverableException;
26+
import java.sql.SQLSyntaxErrorException;
27+
import java.sql.SQLTimeoutException;
28+
import java.sql.SQLTransactionRollbackException;
29+
import java.sql.SQLTransientConnectionException;
2030

2131
import org.junit.jupiter.api.Test;
2232

23-
import org.springframework.dao.ConcurrencyFailureException;
33+
import org.springframework.dao.CannotAcquireLockException;
34+
import org.springframework.dao.DataAccessException;
2435
import org.springframework.dao.DataAccessResourceFailureException;
2536
import org.springframework.dao.DataIntegrityViolationException;
37+
import org.springframework.dao.DuplicateKeyException;
2638
import org.springframework.dao.InvalidDataAccessApiUsageException;
2739
import org.springframework.dao.PermissionDeniedDataAccessException;
40+
import org.springframework.dao.PessimisticLockingFailureException;
2841
import org.springframework.dao.QueryTimeoutException;
2942
import org.springframework.dao.RecoverableDataAccessException;
3043
import org.springframework.dao.TransientDataAccessResourceException;
@@ -34,78 +47,43 @@
3447

3548
/**
3649
* @author Thomas Risberg
50+
* @author Juergen Hoeller
3751
*/
3852
public class SQLExceptionSubclassTranslatorTests {
3953

40-
private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes();
41-
42-
static {
43-
ERROR_CODES.setBadSqlGrammarCodes("1");
54+
@Test
55+
public void exceptionClassTranslation() {
56+
doTest(new SQLDataException("", "", 0), DataIntegrityViolationException.class);
57+
doTest(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class);
58+
doTest(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class);
59+
doTest(new SQLIntegrityConstraintViolationException("", "23505", 0), DuplicateKeyException.class);
60+
doTest(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class);
61+
doTest(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class);
62+
doTest(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class);
63+
doTest(new SQLSyntaxErrorException("", "", 0), BadSqlGrammarException.class);
64+
doTest(new SQLTimeoutException("", "", 0), QueryTimeoutException.class);
65+
doTest(new SQLTransactionRollbackException("", "", 0), PessimisticLockingFailureException.class);
66+
doTest(new SQLTransactionRollbackException("", "40001", 0), CannotAcquireLockException.class);
67+
doTest(new SQLTransientConnectionException("", "", 0), TransientDataAccessResourceException.class);
4468
}
4569

46-
4770
@Test
48-
public void errorCodeTranslation() {
49-
SQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(ERROR_CODES);
50-
51-
SQLException dataIntegrityViolationEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 0);
52-
DataIntegrityViolationException divex = (DataIntegrityViolationException) sext.translate("task", "SQL", dataIntegrityViolationEx);
53-
assertThat(divex.getCause()).isEqualTo(dataIntegrityViolationEx);
54-
55-
SQLException featureNotSupEx = SQLExceptionSubclassFactory.newSQLFeatureNotSupportedException("", "", 0);
56-
InvalidDataAccessApiUsageException idaex = (InvalidDataAccessApiUsageException) sext.translate("task", "SQL", featureNotSupEx);
57-
assertThat(idaex.getCause()).isEqualTo(featureNotSupEx);
58-
59-
SQLException dataIntegrityViolationEx2 = SQLExceptionSubclassFactory.newSQLIntegrityConstraintViolationException("", "", 0);
60-
DataIntegrityViolationException divex2 = (DataIntegrityViolationException) sext.translate("task", "SQL", dataIntegrityViolationEx2);
61-
assertThat(divex2.getCause()).isEqualTo(dataIntegrityViolationEx2);
62-
63-
SQLException permissionDeniedEx = SQLExceptionSubclassFactory.newSQLInvalidAuthorizationSpecException("", "", 0);
64-
PermissionDeniedDataAccessException pdaex = (PermissionDeniedDataAccessException) sext.translate("task", "SQL", permissionDeniedEx);
65-
assertThat(pdaex.getCause()).isEqualTo(permissionDeniedEx);
66-
67-
SQLException dataAccessResourceEx = SQLExceptionSubclassFactory.newSQLNonTransientConnectionException("", "", 0);
68-
DataAccessResourceFailureException darex = (DataAccessResourceFailureException) sext.translate("task", "SQL", dataAccessResourceEx);
69-
assertThat(darex.getCause()).isEqualTo(dataAccessResourceEx);
70-
71-
SQLException badSqlEx2 = SQLExceptionSubclassFactory.newSQLSyntaxErrorException("", "", 0);
72-
BadSqlGrammarException bsgex2 = (BadSqlGrammarException) sext.translate("task", "SQL2", badSqlEx2);
73-
assertThat(bsgex2.getSql()).isEqualTo("SQL2");
74-
assertThat((Object) bsgex2.getSQLException()).isEqualTo(badSqlEx2);
75-
76-
SQLException tranRollbackEx = SQLExceptionSubclassFactory.newSQLTransactionRollbackException("", "", 0);
77-
ConcurrencyFailureException cfex = (ConcurrencyFailureException) sext.translate("task", "SQL", tranRollbackEx);
78-
assertThat(cfex.getCause()).isEqualTo(tranRollbackEx);
79-
80-
SQLException transientConnEx = SQLExceptionSubclassFactory.newSQLTransientConnectionException("", "", 0);
81-
TransientDataAccessResourceException tdarex = (TransientDataAccessResourceException) sext.translate("task", "SQL", transientConnEx);
82-
assertThat(tdarex.getCause()).isEqualTo(transientConnEx);
83-
84-
SQLException transientConnEx2 = SQLExceptionSubclassFactory.newSQLTimeoutException("", "", 0);
85-
QueryTimeoutException tdarex2 = (QueryTimeoutException) sext.translate("task", "SQL", transientConnEx2);
86-
assertThat(tdarex2.getCause()).isEqualTo(transientConnEx2);
87-
88-
SQLException recoverableEx = SQLExceptionSubclassFactory.newSQLRecoverableException("", "", 0);
89-
RecoverableDataAccessException rdaex2 = (RecoverableDataAccessException) sext.translate("task", "SQL", recoverableEx);
90-
assertThat(rdaex2.getCause()).isEqualTo(recoverableEx);
91-
92-
// Test classic error code translation. We should move there next if the exception we pass in is not one
93-
// of the new subclasses.
94-
SQLException sexEct = new SQLException("", "", 1);
95-
BadSqlGrammarException bsgEct = (BadSqlGrammarException) sext.translate("task", "SQL-ECT", sexEct);
96-
assertThat(bsgEct.getSql()).isEqualTo("SQL-ECT");
97-
assertThat((Object) bsgEct.getSQLException()).isEqualTo(sexEct);
98-
71+
public void fallbackStateTranslation() {
9972
// Test fallback. We assume that no database will ever return this error code,
10073
// but 07xxx will be bad grammar picked up by the fallback SQLState translator
101-
SQLException sexFbt = new SQLException("", "07xxx", 666666666);
102-
BadSqlGrammarException bsgFbt = (BadSqlGrammarException) sext.translate("task", "SQL-FBT", sexFbt);
103-
assertThat(bsgFbt.getSql()).isEqualTo("SQL-FBT");
104-
assertThat((Object) bsgFbt.getSQLException()).isEqualTo(sexFbt);
74+
doTest(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class);
10575
// and 08xxx will be data resource failure (non-transient) picked up by the fallback SQLState translator
106-
SQLException sexFbt2 = new SQLException("", "08xxx", 666666666);
107-
DataAccessResourceFailureException darfFbt = (DataAccessResourceFailureException) sext.translate("task", "SQL-FBT2", sexFbt2);
108-
assertThat(darfFbt.getCause()).isEqualTo(sexFbt2);
76+
doTest(new SQLException("", "08xxx", 666666666), DataAccessResourceFailureException.class);
77+
}
78+
79+
80+
private void doTest(SQLException ex, Class<?> dataAccessExceptionType) {
81+
SQLExceptionTranslator translator = new SQLExceptionSubclassTranslator();
82+
DataAccessException dax = translator.translate("task", "SQL", ex);
83+
84+
assertThat(dax).as("Specific translation must not result in null").isNotNull();
85+
assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
86+
assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex);
10987
}
11088

11189
}

0 commit comments

Comments
 (0)