Introduce JdbcTransactionManager with SQLExceptionTranslator support
Closes gh-24064
This commit is contained in:
parent
2aa8aef216
commit
e9cded560d
|
|
@ -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
|
|||
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
|
|||
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
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
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
|
|||
* {@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
|
|||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
|||
* @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
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
/**
|
||||
* @author Juergen Hoeller
|
||||
* @since 04.07.2003
|
||||
* @see org.springframework.jdbc.support.JdbcTransactionManagerTests
|
||||
*/
|
||||
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 {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 {
|
|||
@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 {
|
|||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
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 {
|
|||
|
||||
|
||||
@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 {
|
|||
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…
Reference in New Issue