diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java index 6118e00455c..2601d9c857a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java @@ -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}). * + *
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}. + * * @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. + *
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. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java index cd16f97ec1c..44b8838b210 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java @@ -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; } /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java new file mode 100644 index 00000000000..aa48eb8d0ef --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java @@ -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. + * + *
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}. + * + *
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. + *
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. + *
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. + *
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); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java index 5f64bfaed10..349ea1d29c7 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java @@ -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 diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java index 0b401b2a87c..84e5c3b1a58 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java @@ -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 { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcTransactionManagerTests.java new file mode 100644 index 00000000000..79e62959953 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcTransactionManagerTests.java @@ -0,0 +1,1845 @@ +/* + * 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.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Savepoint; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.UncategorizedSQLException; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.ConnectionProxy; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.TransactionTimedOutException; +import org.springframework.transaction.UnexpectedRollbackException; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.core.testfixture.TestGroup.PERFORMANCE; + +/** + * @author Juergen Hoeller + * @since 5.3 + * @see org.springframework.jdbc.datasource.DataSourceTransactionManagerTests + */ +public class JdbcTransactionManagerTests { + + private DataSource ds; + + private Connection con; + + private JdbcTransactionManager tm; + + + @BeforeEach + public void setup() throws Exception { + ds = mock(DataSource.class); + con = mock(Connection.class); + given(ds.getConnection()).willReturn(con); + tm = new JdbcTransactionManager(ds); + } + + @AfterEach + public void verifyTransactionSynchronizationManagerState() { + assertThat(TransactionSynchronizationManager.getResourceMap().isEmpty()).isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + + + @Test + public void testTransactionCommitWithAutoCommitTrue() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, false, false); + } + + @Test + public void testTransactionCommitWithAutoCommitFalse() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, false, false); + } + + @Test + public void testTransactionCommitWithAutoCommitTrueAndLazyConnection() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, true, false); + } + + @Test + public void testTransactionCommitWithAutoCommitFalseAndLazyConnection() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, true, false); + } + + @Test + public void testTransactionCommitWithAutoCommitTrueAndLazyConnectionAndStatementCreated() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, true, true); + } + + @Test + public void testTransactionCommitWithAutoCommitFalseAndLazyConnectionAndStatementCreated() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, true, true); + } + + private void doTestTransactionCommitRestoringAutoCommit( + boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + + if (lazyConnection) { + given(con.getAutoCommit()).willReturn(autoCommit); + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + given(con.getWarnings()).willThrow(new SQLException()); + } + + if (!lazyConnection || createStatement) { + given(con.getAutoCommit()).willReturn(autoCommit); + } + + final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + tm = new JdbcTransactionManager(dsToUse); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + Connection tCon = DataSourceUtils.getConnection(dsToUse); + try { + if (createStatement) { + tCon.createStatement(); + } + else { + tCon.getWarnings(); + tCon.clearWarnings(); + } + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + + if (autoCommit && (!lazyConnection || createStatement)) { + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + } + if (createStatement) { + verify(con, times(2)).close(); + } + else { + verify(con).close(); + } + } + + @Test + public void testTransactionRollbackWithAutoCommitTrue() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, false, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalse() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, false, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitTrueAndLazyConnection() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, true, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalseAndLazyConnection() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, true, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitTrueAndLazyConnectionAndCreateStatement() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, true, true); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalseAndLazyConnectionAndCreateStatement() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, true, true); + } + + private void doTestTransactionRollbackRestoringAutoCommit( + boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + + if (lazyConnection) { + given(con.getAutoCommit()).willReturn(autoCommit); + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + } + + if (!lazyConnection || createStatement) { + given(con.getAutoCommit()).willReturn(autoCommit); + } + + final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + tm = new JdbcTransactionManager(dsToUse); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + final RuntimeException ex = new RuntimeException("Application exception"); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Connection con = DataSourceUtils.getConnection(dsToUse); + if (createStatement) { + try { + con.createStatement(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + throw ex; + } + })) + .isEqualTo(ex); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + + if (autoCommit && (!lazyConnection || createStatement)) { + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + } + if (createStatement) { + verify(con, times(2)).close(); + } + else { + verify(con).close(); + } + } + + @Test + public void testTransactionRollbackOnly() throws Exception { + tm.setTransactionSynchronization(JdbcTransactionManager.SYNCHRONIZATION_NEVER); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + ConnectionHolder conHolder = new ConnectionHolder(con, true); + TransactionSynchronizationManager.bindResource(ds, conHolder); + final RuntimeException ex = new RuntimeException("Application exception"); + try { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + throw ex; + } + }); + fail("Should have thrown RuntimeException"); + } + catch (RuntimeException ex2) { + // expected + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + assertThat(ex2).as("Correct exception thrown").isEqualTo(ex); + } + finally { + TransactionSynchronizationManager.unbindResource(ds); + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testParticipatingTransactionWithRollbackOnly() throws Exception { + doTestParticipatingTransactionWithRollbackOnly(false); + } + + @Test + public void testParticipatingTransactionWithRollbackOnlyAndFailEarly() throws Exception { + doTestParticipatingTransactionWithRollbackOnly(true); + } + + private void doTestParticipatingTransactionWithRollbackOnly(boolean failEarly) throws Exception { + given(con.isReadOnly()).willReturn(false); + if (failEarly) { + tm.setFailEarlyOnGlobalRollbackOnly(true); + } + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); + TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_ROLLED_BACK); + TransactionSynchronizationManager.registerSynchronization(synch); + + boolean outerTransactionBoundaryReached = false; + try { + assertThat(ts.isNewTransaction()).as("Is new transaction").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + status.setRollbackOnly(); + } + }); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + + outerTransactionBoundaryReached = true; + tm.commit(ts); + + fail("Should have thrown UnexpectedRollbackException"); + } + catch (UnexpectedRollbackException ex) { + // expected + if (!outerTransactionBoundaryReached) { + tm.rollback(ts); + } + if (failEarly) { + assertThat(outerTransactionBoundaryReached).isFalse(); + } + else { + assertThat(outerTransactionBoundaryReached).isTrue(); + } + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isFalse(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isFalse(); + assertThat(synch.afterCompletionCalled).isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithIncompatibleIsolationLevel() throws Exception { + tm.setValidateExistingTransaction(true); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { + final TransactionTemplate tt = new TransactionTemplate(tm); + final TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithIncompatibleReadOnly() throws Exception { + willThrow(new SQLException("read-only not supported")).given(con).setReadOnly(true); + tm.setValidateExistingTransaction(true); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setReadOnly(true); + final TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setReadOnly(false); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithTransactionStartedFromSynch() throws Exception { + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { + @Override + protected void doAfterCompletion(int status) { + super.doAfterCompletion(status); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + } + }); + TransactionSynchronizationManager.registerSynchronization( + new TransactionSynchronizationAdapter() { + }); + } + }; + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isTrue(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isTrue(); + assertThat(synch.afterCompletionCalled).isTrue(); + boolean condition3 = synch.afterCompletionException instanceof IllegalStateException; + assertThat(condition3).isTrue(); + verify(con, times(2)).commit(); + verify(con, times(2)).close(); + } + + @Test + public void testParticipatingTransactionWithDifferentConnectionObtainedFromSynch() throws Exception { + DataSource ds2 = mock(DataSource.class); + final Connection con2 = mock(Connection.class); + given(ds2.getConnection()).willReturn(con2); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { + @Override + protected void doAfterCompletion(int status) { + super.doAfterCompletion(status); + Connection con = DataSourceUtils.getConnection(ds2); + DataSourceUtils.releaseConnection(con, ds2); + } + }; + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isTrue(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isTrue(); + assertThat(synch.afterCompletionCalled).isTrue(); + assertThat(synch.afterCompletionException).isNull(); + verify(con).commit(); + verify(con).close(); + verify(con2).close(); + } + + @Test + public void testParticipatingTransactionWithRollbackOnlyAndInnerSynch() throws Exception { + tm.setTransactionSynchronization(JdbcTransactionManager.SYNCHRONIZATION_NEVER); + JdbcTransactionManager tm2 = new JdbcTransactionManager(ds); + // tm has no synch enabled (used at outer level), tm2 has synch enabled (inner level) + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_UNKNOWN); + + assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> { + assertThat(ts.isNewTransaction()).as("Is new transaction").isTrue(); + final TransactionTemplate tt = new TransactionTemplate(tm2); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + status.setRollbackOnly(); + } + }); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + tm.commit(ts); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isFalse(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isFalse(); + assertThat(synch.afterCompletionCalled).isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).commit(); + verify(con, times(2)).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransactionAndUnrelatedDataSource() throws Exception { + Connection con2 = mock(Connection.class); + final DataSource ds2 = mock(DataSource.class); + given(ds2.getConnection()).willReturn(con2); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + PlatformTransactionManager tm2 = new JdbcTransactionManager(ds2); + final TransactionTemplate tt2 = new TransactionTemplate(tm2); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + boolean condition4 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition4).as("Hasn't thread connection").isTrue(); + boolean condition3 = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + verify(con2).rollback(); + verify(con2).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransactionAndUnrelatedFailingDataSource() throws Exception { + final DataSource ds2 = mock(DataSource.class); + SQLException failure = new SQLException(); + given(ds2.getConnection()).willThrow(failure); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + JdbcTransactionManager tm2 = new JdbcTransactionManager(ds2); + tm2.setTransactionSynchronization(JdbcTransactionManager.SYNCHRONIZATION_NEVER); + final TransactionTemplate tt2 = new TransactionTemplate(tm2); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + boolean condition4 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition4).as("Hasn't thread connection").isTrue(); + boolean condition3 = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(CannotCreateTransactionException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + } + })).withCause(failure); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationNotSupportedWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testPropagationNeverWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + fail("Should have thrown IllegalTransactionStateException"); + } + }); + fail("Should have thrown IllegalTransactionStateException"); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationSupportsAndRequiresNew() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con); + } + }); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testPropagationSupportsAndRequiresNewWithEarlyAccess() throws Exception { + final Connection con1 = mock(Connection.class); + final Connection con2 = mock(Connection.class); + given(ds.getConnection()).willReturn(con1, con2); + + final + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con2); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con2); + } + }); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con1).close(); + verify(con2).commit(); + verify(con2).close(); + } + + @Test + public void testTransactionWithIsolationAndReadOnly() throws Exception { + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + given(con.getAutoCommit()).willReturn(true); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + tt.setReadOnly(true); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isTrue(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + // something transactional + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setReadOnly(true); + ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + ordered.verify(con).setReadOnly(false); + verify(con).close(); + } + + @Test + public void testTransactionWithEnforceReadOnly() throws Exception { + tm.setEnforceReadOnly(true); + + given(con.getAutoCommit()).willReturn(true); + Statement stmt = mock(Statement.class); + given(con.createStatement()).willReturn(stmt); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt.setReadOnly(true); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isTrue(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + // something transactional + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con, stmt); + ordered.verify(con).setReadOnly(true); + ordered.verify(con).setAutoCommit(false); + ordered.verify(stmt).executeUpdate("SET TRANSACTION READ ONLY"); + ordered.verify(stmt).close(); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + ordered.verify(con).setReadOnly(false); + ordered.verify(con).close(); + } + + @Test + public void testTransactionWithLongTimeout() throws Exception { + doTestTransactionWithTimeout(10); + } + + @Test + public void testTransactionWithShortTimeout() throws Exception { + doTestTransactionWithTimeout(1); + } + + @EnabledForTestGroups(PERFORMANCE) + private void doTestTransactionWithTimeout(int timeout) throws Exception { + PreparedStatement ps = mock(PreparedStatement.class); + given(con.getAutoCommit()).willReturn(true); + given(con.prepareStatement("some SQL statement")).willReturn(ps); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setTimeout(timeout); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + try { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + try { + Thread.sleep(1500); + } + catch (InterruptedException ex) { + } + try { + Connection con = DataSourceUtils.getConnection(ds); + PreparedStatement ps = con.prepareStatement("some SQL statement"); + DataSourceUtils.applyTransactionTimeout(ps, ds); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("", ex); + } + } + }); + if (timeout <= 1) { + fail("Should have thrown TransactionTimedOutException"); + } + } + catch (TransactionTimedOutException ex) { + if (timeout <= 1) { + // expected + } + else { + throw ex; + } + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + if (timeout > 1) { + verify(ps).setQueryTimeout(timeout - 1); + verify(con).commit(); + } + else { + verify(con).rollback(); + } + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionAwareDataSourceProxy() throws Exception { + given(con.getAutoCommit()).willReturn(true); + given(con.getWarnings()).willThrow(new SQLException()); + + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + try { + Connection tCon = dsProxy.getConnection(); + tCon.getWarnings(); + tCon.clearWarnings(); + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { + given(con.getAutoCommit()).willReturn(true); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con, times(2)).close(); + } + + @Test + public void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Exception { + given(con.getAutoCommit()).willReturn(true); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + dsProxy.setReobtainTransactionalConnections(true); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con, times(2)).close(); + } + + /** + * Test behavior if the first operation on a connection (getAutoCommit) throws SQLException. + */ + @Test + public void testTransactionWithExceptionOnBegin() throws Exception { + willThrow(new SQLException("Cannot begin")).given(con).getAutoCommit(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(CannotCreateTransactionException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnCommit() throws Exception { + willThrow(new SQLException("Cannot commit")).given(con).commit(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithDataAccessExceptionOnCommit() throws Exception { + willThrow(new SQLException("Cannot commit")).given(con).commit(); + tm.setExceptionTranslator((task, sql, ex) -> new ConcurrencyFailureException(task)); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithDataAccessExceptionOnCommitFromLazyExceptionTranslator() throws Exception { + willThrow(new SQLException("Cannot commit", "40")).given(con).commit(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnCommitAndRollbackOnCommitFailure() throws Exception { + willThrow(new SQLException("Cannot commit")).given(con).commit(); + + tm.setRollbackOnCommitFailure(true); + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnRollback() throws Exception { + given(con.getAutoCommit()).willReturn(true); + willThrow(new SQLException("Cannot rollback")).given(con).rollback(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionWithDataAccessExceptionOnRollback() throws Exception { + given(con.getAutoCommit()).willReturn(true); + willThrow(new SQLException("Cannot rollback")).given(con).rollback(); + tm.setExceptionTranslator((task, sql, ex) -> new ConcurrencyFailureException(task)); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionWithDataAccessExceptionOnRollbackFromLazyExceptionTranslator() throws Exception { + given(con.getAutoCommit()).willReturn(true); + willThrow(new SQLException("Cannot rollback", "40")).given(con).rollback(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationSupports() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testTransactionWithPropagationNotSupported() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testTransactionWithPropagationNever() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testExistingTransactionWithPropagationNested() throws Exception { + doTestExistingTransactionWithPropagationNested(1); + } + + @Test + public void testExistingTransactionWithPropagationNestedTwice() throws Exception { + doTestExistingTransactionWithPropagationNested(2); + } + + private void doTestExistingTransactionWithPropagationNested(final int count) throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + for (int i = 1; i <= count; i++) { + given(con.setSavepoint(ConnectionHolder.SAVEPOINT_NAME_PREFIX + i)).willReturn(sp); + } + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + for (int i = 0; i < count; i++) { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + } + }); + } + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con, times(count)).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRequiredRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + assertThatIllegalStateException().isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + TransactionTemplate ntt = new TransactionTemplate(tm); + ntt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Isn't new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Is regular transaction").isTrue(); + throw new IllegalStateException(); + } + }); + } + })); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRequiredRollbackOnly() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + TransactionTemplate ntt = new TransactionTemplate(tm); + ntt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Isn't new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Is regular transaction").isTrue(); + status.setRollbackOnly(); + } + }); + } + })); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithManualSavepoint() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Object savepoint = status.createSavepoint(); + status.releaseSavepoint(savepoint); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + verify(ds).getConnection(); + } + + @Test + public void testExistingTransactionWithManualSavepointAndRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Object savepoint = status.createSavepoint(); + status.rollbackToSavepoint(savepoint); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationNested() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationNestedAndRollback() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + status.setRollbackOnly(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + + private static class TestTransactionSynchronization implements TransactionSynchronization { + + private DataSource dataSource; + + private int status; + + public boolean beforeCommitCalled; + + public boolean beforeCompletionCalled; + + public boolean afterCommitCalled; + + public boolean afterCompletionCalled; + + public Throwable afterCompletionException; + + public TestTransactionSynchronization(DataSource dataSource, int status) { + this.dataSource = dataSource; + this.status = status; + } + + @Override + public void suspend() { + } + + @Override + public void resume() { + } + + @Override + public void flush() { + } + + @Override + public void beforeCommit(boolean readOnly) { + if (this.status != TransactionSynchronization.STATUS_COMMITTED) { + fail("Should never be called"); + } + assertThat(this.beforeCommitCalled).isFalse(); + this.beforeCommitCalled = true; + } + + @Override + public void beforeCompletion() { + assertThat(this.beforeCompletionCalled).isFalse(); + this.beforeCompletionCalled = true; + } + + @Override + public void afterCommit() { + if (this.status != TransactionSynchronization.STATUS_COMMITTED) { + fail("Should never be called"); + } + assertThat(this.afterCommitCalled).isFalse(); + this.afterCommitCalled = true; + } + + @Override + public void afterCompletion(int status) { + try { + doAfterCompletion(status); + } + catch (Throwable ex) { + this.afterCompletionException = ex; + } + } + + protected void doAfterCompletion(int status) { + assertThat(this.afterCompletionCalled).isFalse(); + this.afterCompletionCalled = true; + assertThat(status == this.status).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(this.dataSource)).isTrue(); + } + } + +} 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 index 0a712bc4265..608e0b5d331 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java @@ -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(); } } 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 c4556319cce..98baf1ab7f9 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-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);