Browse Source

Introduce JdbcTransactionManager with SQLExceptionTranslator support

Closes gh-24064
pull/25021/head
Juergen Hoeller 6 years ago
parent
commit
e9cded560d
  1. 24
      spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java
  2. 15
      spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java
  3. 174
      spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java
  4. 6
      spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java
  5. 6
      spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java
  6. 1845
      spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcTransactionManagerTests.java
  7. 28
      spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java
  8. 21
      spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java

24
spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java

@ -98,6 +98,10 @@ import org.springframework.util.Assert; @@ -98,6 +98,10 @@ import org.springframework.util.Assert;
* setup analogous to {@code JtaTransactionManager}, in particular with respect to
* lazily registered ORM resources (e.g. a Hibernate {@code Session}).
*
* <p><b>NOTE: As of 5.3, {@link org.springframework.jdbc.support.JdbcTransactionManager}
* is available as an extended subclass which includes commit/rollback exception
* translation, aligned with {@link org.springframework.jdbc.core.JdbcTemplate}.</b>
*
* @author Juergen Hoeller
* @since 02.05.2003
* @see #setNestedTransactionAllowed
@ -332,7 +336,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan @@ -332,7 +336,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
throw translateException("JDBC commit", ex);
}
}
@ -347,7 +351,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan @@ -347,7 +351,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
con.rollback();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
throw translateException("JDBC rollback", ex);
}
}
@ -418,6 +422,22 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan @@ -418,6 +422,22 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
}
}
/**
* Translate the given JDBC commit/rollback exception to a common Spring
* exception to propagate from the {@link #commit}/{@link #rollback} call.
* <p>The default implementation throws a {@link TransactionSystemException}.
* Subclasses may specifically identify concurrency failures etc.
* @param task the task description (commit or rollback)
* @param ex the SQLException thrown from commit/rollback
* @return the translated exception to throw, either a
* {@link org.springframework.dao.DataAccessException} or a
* {@link org.springframework.transaction.TransactionException}
* @since 5.3
*/
protected RuntimeException translateException(String task, SQLException ex) {
return new TransactionSystemException(task + " failed", ex);
}
/**
* DataSource transaction object, representing a ConnectionHolder.

15
spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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,8 +22,6 @@ import org.apache.commons.logging.Log; @@ -22,8 +22,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -65,7 +63,7 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep @@ -65,7 +63,7 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep
* {@link #getFallbackTranslator() fallback translator} if necessary.
*/
@Override
@NonNull
@Nullable
public DataAccessException translate(String task, @Nullable String sql, SQLException ex) {
Assert.notNull(ex, "Cannot translate a null SQLException");
@ -78,15 +76,10 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep @@ -78,15 +76,10 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep
// Looking for a fallback...
SQLExceptionTranslator fallback = getFallbackTranslator();
if (fallback != null) {
dae = fallback.translate(task, sql, ex);
if (dae != null) {
// Fallback exception match found.
return dae;
}
return fallback.translate(task, sql, ex);
}
// We couldn't identify it more precisely.
return new UncategorizedSQLException(task, sql, ex);
return null;
}
/**

174
spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java

@ -0,0 +1,174 @@ @@ -0,0 +1,174 @@
/*
* Copyright 2002-2020 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 javax.sql.DataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.lang.Nullable;
/**
* {@link JdbcAccessor}-aligned subclass of the plain {@link DataSourceTransactionManager},
* adding common JDBC exception translation for the commit and rollback step.
* Typically used in combination with {@link org.springframework.jdbc.core.JdbcTemplate}
* which applies the same {@link SQLExceptionTranslator} infrastructure by default.
*
* <p>Exception translation is specifically relevant for commit steps in serializable
* transactions (e.g. on Postgres) where concurrency failures may occur late on commit.
* This allows for throwing {@link org.springframework.dao.ConcurrencyFailureException} to
* callers instead of {@link org.springframework.transaction.TransactionSystemException}.
*
* <p>Analogous to {@code HibernateTransactionManager} and {@code JpaTransactionManager},
* this transaction manager may throw {@link DataAccessException} from {@link #commit}
* and possibly also from {@link #rollback}. Calling code should be prepared for handling
* such exceptions next to {@link org.springframework.transaction.TransactionException},
* which is generally sensible since {@code TransactionSynchronization} implementations
* may also throw such exceptions in their {@code flush} and {@code beforeCommit} phases.
*
* @author Juergen Hoeller
* @since 5.3
* @see DataSourceTransactionManager
* @see #setDataSource
* @see #setExceptionTranslator
*/
@SuppressWarnings("serial")
public class JdbcTransactionManager extends DataSourceTransactionManager {
@Nullable
private volatile SQLExceptionTranslator exceptionTranslator;
private boolean lazyInit = true;
/**
* Create a new JdbcTransactionManager instance.
* A DataSource has to be set to be able to use it.
* @see #setDataSource
*/
public JdbcTransactionManager() {
super();
}
/**
* Create a new JdbcTransactionManager instance.
* @param dataSource the JDBC DataSource to manage transactions for
*/
public JdbcTransactionManager(DataSource dataSource) {
this();
setDataSource(dataSource);
afterPropertiesSet();
}
/**
* Specify the database product name for the DataSource that this transaction manager
* uses. This allows to initialize an SQLErrorCodeSQLExceptionTranslator without
* obtaining a Connection from the DataSource to get the meta-data.
* @param dbName the database product name that identifies the error codes entry
* @see JdbcAccessor#setDatabaseProductName
* @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
*/
public void setDatabaseProductName(String dbName) {
this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName);
}
/**
* Set the exception translator for this instance.
* <p>If no custom translator is provided, a default
* {@link SQLErrorCodeSQLExceptionTranslator} is used
* which examines the SQLException's vendor-specific error code.
* @see JdbcAccessor#setExceptionTranslator
* @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
*/
public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
}
/**
* Return the exception translator for this instance.
* <p>Creates a default {@link SQLErrorCodeSQLExceptionTranslator}
* for the specified DataSource if none set.
* @see #getDataSource()
*/
public SQLExceptionTranslator getExceptionTranslator() {
SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator != null) {
return exceptionTranslator;
}
synchronized (this) {
exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator == null) {
exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(obtainDataSource());
this.exceptionTranslator = exceptionTranslator;
}
return exceptionTranslator;
}
}
/**
* Set whether to lazily initialize the SQLExceptionTranslator for this transaction manager,
* on first encounter of an SQLException. Default is "true"; can be switched to
* "false" for initialization on startup.
* <p>Early initialization just applies if {@code afterPropertiesSet()} is called.
* @see #getExceptionTranslator()
* @see #afterPropertiesSet()
*/
public void setLazyInit(boolean lazyInit) {
this.lazyInit = lazyInit;
}
/**
* Return whether to lazily initialize the SQLExceptionTranslator for this transaction manager.
* @see #getExceptionTranslator()
*/
public boolean isLazyInit() {
return this.lazyInit;
}
/**
* Eagerly initialize the exception translator, if demanded,
* creating a default one for the specified DataSource if none set.
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
if (!isLazyInit()) {
getExceptionTranslator();
}
}
/**
* This implementation attempts to use the {@link SQLExceptionTranslator},
* falling back to a {@link org.springframework.transaction.TransactionSystemException}.
* @see #getExceptionTranslator()
* @see DataSourceTransactionManager#translateException
*/
@Override
protected RuntimeException translateException(String task, SQLException ex) {
DataAccessException dae = getExceptionTranslator().translate(task, null, ex);
if (dae != null) {
return dae;
}
return super.translateException(task, ex);
}
}

6
spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@ -49,9 +49,7 @@ public interface SQLExceptionTranslator { @@ -49,9 +49,7 @@ public interface SQLExceptionTranslator {
* @param sql the SQL query or update that caused the problem (if known)
* @param ex the offending {@code SQLException}
* @return the DataAccessException wrapping the {@code SQLException},
* or {@code null} if no translation could be applied
* (in a custom translator; the default translators always throw an
* {@link org.springframework.jdbc.UncategorizedSQLException} in such a case)
* or {@code null} if no specific translation could be applied
* @see org.springframework.dao.DataAccessException#getRootCause()
*/
@Nullable

6
spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@ -63,6 +63,7 @@ import static org.springframework.core.testfixture.TestGroup.PERFORMANCE; @@ -63,6 +63,7 @@ import static org.springframework.core.testfixture.TestGroup.PERFORMANCE;
/**
* @author Juergen Hoeller
* @since 04.07.2003
* @see org.springframework.jdbc.support.JdbcTransactionManagerTests
*/
public class DataSourceTransactionManagerTests {
@ -284,8 +285,7 @@ public class DataSourceTransactionManagerTests { @@ -284,8 +285,7 @@ public class DataSourceTransactionManagerTests {
boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive();
assertThat(condition1).as("Synchronization not active").isTrue();
ConnectionHolder conHolder = new ConnectionHolder(con);
conHolder.setTransactionActive(true);
ConnectionHolder conHolder = new ConnectionHolder(con, true);
TransactionSynchronizationManager.bindResource(ds, conHolder);
final RuntimeException ex = new RuntimeException("Application exception");
try {

1845
spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcTransactionManagerTests.java

File diff suppressed because it is too large Load Diff

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

@ -21,7 +21,6 @@ import java.sql.SQLException; @@ -21,7 +21,6 @@ import java.sql.SQLException;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.UncategorizedSQLException;
import static org.assertj.core.api.Assertions.assertThat;
@ -54,14 +53,7 @@ public class SQLStateExceptionTranslatorTests { @@ -54,14 +53,7 @@ public class SQLStateExceptionTranslatorTests {
@Test
public void invalidSqlStateCode() {
SQLException sex = new SQLException("Message", "NO SUCH CODE", 1);
try {
throw this.trans.translate("task", sql, sex);
}
catch (UncategorizedSQLException ex) {
// OK
assertThat(sql.equals(ex.getSql())).as("SQL is correct").isTrue();
assertThat(sex.equals(ex.getSQLException())).as("Exception matches").isTrue();
}
assertThat(this.trans.translate("task", sql, sex)).isNull();
}
/**
@ -72,26 +64,14 @@ public class SQLStateExceptionTranslatorTests { @@ -72,26 +64,14 @@ public class SQLStateExceptionTranslatorTests {
@Test
public void malformedSqlStateCodes() {
SQLException sex = new SQLException("Message", null, 1);
testMalformedSqlStateCode(sex);
assertThat(this.trans.translate("task", sql, sex)).isNull();
sex = new SQLException("Message", "", 1);
testMalformedSqlStateCode(sex);
assertThat(this.trans.translate("task", sql, sex)).isNull();
// One char's not allowed
sex = new SQLException("Message", "I", 1);
testMalformedSqlStateCode(sex);
}
private void testMalformedSqlStateCode(SQLException sex) {
try {
throw this.trans.translate("task", sql, sex);
}
catch (UncategorizedSQLException ex) {
// OK
assertThat(sql.equals(ex.getSql())).as("SQL is correct").isTrue();
assertThat(sex.equals(ex.getSQLException())).as("Exception matches").isTrue();
}
assertThat(this.trans.translate("task", sql, sex)).isNull();
}
}

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@ -26,7 +26,6 @@ import org.springframework.dao.DataAccessResourceFailureException; @@ -26,7 +26,6 @@ import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.UncategorizedSQLException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -46,39 +45,39 @@ public class SQLStateSQLExceptionTranslatorTests { @@ -46,39 +45,39 @@ public class SQLStateSQLExceptionTranslatorTests {
@Test
public void testTranslateNullException() throws Exception {
public void testTranslateNullException() {
assertThatIllegalArgumentException().isThrownBy(() ->
new SQLStateSQLExceptionTranslator().translate("", "", null));
}
@Test
public void testTranslateBadSqlGrammar() throws Exception {
public void testTranslateBadSqlGrammar() {
doTest("07", BadSqlGrammarException.class);
}
@Test
public void testTranslateDataIntegrityViolation() throws Exception {
public void testTranslateDataIntegrityViolation() {
doTest("23", DataIntegrityViolationException.class);
}
@Test
public void testTranslateDataAccessResourceFailure() throws Exception {
public void testTranslateDataAccessResourceFailure() {
doTest("53", DataAccessResourceFailureException.class);
}
@Test
public void testTranslateTransientDataAccessResourceFailure() throws Exception {
public void testTranslateTransientDataAccessResourceFailure() {
doTest("S1", TransientDataAccessResourceException.class);
}
@Test
public void testTranslateConcurrencyFailure() throws Exception {
public void testTranslateConcurrencyFailure() {
doTest("40", ConcurrencyFailureException.class);
}
@Test
public void testTranslateUncategorized() throws Exception {
doTest("00000000", UncategorizedSQLException.class);
public void testTranslateUncategorized() {
assertThat(new SQLStateSQLExceptionTranslator().translate("", "", new SQLException(REASON, "00000000"))).isNull();
}
@ -86,7 +85,7 @@ public class SQLStateSQLExceptionTranslatorTests { @@ -86,7 +85,7 @@ public class SQLStateSQLExceptionTranslatorTests {
SQLException ex = new SQLException(REASON, sqlState);
SQLExceptionTranslator translator = new SQLStateSQLExceptionTranslator();
DataAccessException dax = translator.translate(TASK, SQL, ex);
assertThat(dax).as("Translation must *never* result in a null DataAccessException being returned.").isNotNull();
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);

Loading…
Cancel
Save