diff --git a/hibernate-core/src/test/java/org/hibernate/test/resource/transaction/jdbc/autocommit/AbstractSkipAutoCommitTest.java b/hibernate-core/src/test/java/org/hibernate/test/resource/transaction/jdbc/autocommit/AbstractSkipAutoCommitTest.java index b5aef8c98d..f29eb6cf6a 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/resource/transaction/jdbc/autocommit/AbstractSkipAutoCommitTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/resource/transaction/jdbc/autocommit/AbstractSkipAutoCommitTest.java @@ -6,18 +6,30 @@ */ package org.hibernate.test.resource.transaction.jdbc.autocommit; +import java.io.Closeable; +import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import javax.persistence.Entity; +import javax.persistence.EntityTransaction; import javax.persistence.Id; import javax.sql.DataSource; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.transaction.spi.TransactionImplementor; +import org.hibernate.jpa.boot.spi.Bootstrap; import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; +import org.hibernate.testing.junit4.BaseUnitTestCase; +import org.hibernate.testing.transaction.TransactionUtil2; import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; @@ -30,10 +42,17 @@ import static org.mockito.Mockito.verify; /** * @author Vlad Mihalcea */ -public abstract class AbstractSkipAutoCommitTest extends BaseEntityManagerFunctionalTestCase { +public abstract class AbstractSkipAutoCommitTest extends BaseUnitTestCase { - private PreparedStatementSpyConnectionProvider connectionProvider = - new PreparedStatementSpyConnectionProvider() { + private PreparedStatementSpyConnectionProvider connectionProvider; + private DataSource dataSource; + private SessionFactoryImplementor emf; + + @Before + public void createEntityManagerFactory() { + Map config = new HashMap<>(); + + connectionProvider = new PreparedStatementSpyConnectionProvider() { @Override protected Connection actualConnection() throws SQLException { Connection connection = super.actualConnection(); @@ -42,51 +61,93 @@ public abstract class AbstractSkipAutoCommitTest extends BaseEntityManagerFuncti } }; - @Override - protected Map getConfig() { - Map config = super.getConfig(); + dataSource = dataSource(); - config.put( AvailableSettings.DATASOURCE, dataSource() ); - config.put( AvailableSettings.CONNECTION_PROVIDER_DISABLES_AUTOCOMMIT, Boolean.TRUE ); config.put( AvailableSettings.CONNECTION_PROVIDER, connectionProvider ); + config.put( AvailableSettings.DATASOURCE, dataSource ); + config.put( AvailableSettings.CONNECTION_PROVIDER_DISABLES_AUTOCOMMIT, Boolean.TRUE ); + config.put( AvailableSettings.HBM2DDL_AUTO, "create-drop" ); - return config; + config.put( AvailableSettings.JPA_TRANSACTION_COMPLIANCE, "false" ); + + emf = Bootstrap.getEntityManagerFactoryBuilder( + new BaseEntityManagerFunctionalTestCase.TestingPersistenceUnitDescriptorImpl( getClass().getSimpleName() ) { + @Override + public List getManagedClassNames() { + return Collections.singletonList( City.class.getName() ); + } + }, + config + ).build().unwrap( SessionFactoryImplementor.class ); + if ( emf == null ) { + throw new RuntimeException( "Could not build EMF" ); + } } protected abstract DataSource dataSource(); - @Override + @After public void releaseResources() { - super.releaseResources(); - connectionProvider.stop(); + if ( connectionProvider != null ) { + connectionProvider.stop(); + } + + // todo : somewhay to stop/close DataSource if not Closeable? + if ( dataSource instanceof Closeable ) { + try { + ( (Closeable) dataSource ).close(); + } + catch (IOException e) { + log.debugf( "Unable to release DataSource : %s", dataSource ); + } + + if ( emf != null ) { + emf.close(); + } + } } - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { - City.class, - }; + @Test + public void testRollbackOnNonJtaDataSourceWithAutoCommitConnection() { + TransactionUtil2.inEntityManager( + emf, + entityManager -> { + final EntityTransaction txn = entityManager.getTransaction(); +// txn.begin(); + + final TransactionImplementor hibernateTxn = (TransactionImplementor) txn; + hibernateTxn.markRollbackOnly(); + + txn.rollback(); + } + ); } @Test public void test() { connectionProvider.clear(); - doInJPA( this::entityManagerFactory, entityManager -> { - City city = new City(); - city.setId( 1L ); - city.setName( "Cluj-Napoca" ); - entityManager.persist( city ); + doInJPA( + () -> emf, + entityManager -> { + City city = new City(); + city.setId( 1L ); + city.setName( "Cluj-Napoca" ); + entityManager.persist( city ); - assertTrue( connectionProvider.getAcquiredConnections().isEmpty() ); - assertTrue( connectionProvider.getReleasedConnections().isEmpty() ); - } ); + assertTrue( connectionProvider.getAcquiredConnections().isEmpty() ); + assertTrue( connectionProvider.getReleasedConnections().isEmpty() ); + } + ); verifyConnections(); connectionProvider.clear(); - doInJPA( this::entityManagerFactory, entityManager -> { - City city = entityManager.find( City.class, 1L ); - assertEquals( "Cluj-Napoca", city.getName() ); - } ); + doInJPA( + () -> emf, + entityManager -> { + City city = entityManager.find( City.class, 1L ); + assertEquals( "Cluj-Napoca", city.getName() ); + } + ); verifyConnections(); } diff --git a/hibernate-core/src/test/java/org/hibernate/test/resource/transaction/jdbc/autocommit/MySQLSkipAutoCommitTest.java b/hibernate-core/src/test/java/org/hibernate/test/resource/transaction/jdbc/autocommit/MySQLSkipAutoCommitTest.java index 1b43cf6a98..6564a9963a 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/resource/transaction/jdbc/autocommit/MySQLSkipAutoCommitTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/resource/transaction/jdbc/autocommit/MySQLSkipAutoCommitTest.java @@ -10,6 +10,7 @@ import javax.sql.DataSource; import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Environment; +import org.hibernate.dialect.Dialect; import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.MySQLDialect; @@ -26,7 +27,7 @@ public class MySQLSkipAutoCommitTest extends AbstractSkipAutoCommitTest { @Override protected DataSource dataSource() { DataSource dataSource = ReflectionUtil.newInstance( "com.mysql.cj.jdbc.MysqlDataSource" ); - if ( getDialect() instanceof MariaDBDialect ) { + if ( Dialect.getDialect() instanceof MariaDBDialect ) { dataSource = ReflectionUtil.newInstance( "org.mariadb.jdbc.MariaDbDataSource" ); } ReflectionUtil.setProperty( dataSource, "url", Environment.getProperties().getProperty( AvailableSettings.URL ) ); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/transaction/TransactionUtil2.java b/hibernate-testing/src/main/java/org/hibernate/testing/transaction/TransactionUtil2.java index d91a3f64e8..810cd6d2e6 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/transaction/TransactionUtil2.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/transaction/TransactionUtil2.java @@ -7,6 +7,9 @@ package org.hibernate.testing.transaction; import java.util.function.Consumer; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityTransaction; import org.hibernate.Transaction; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -34,6 +37,18 @@ public class TransactionUtil2 { } } + public static void inEntityManager(EntityManagerFactory emf, Consumer action) { + log.trace( "#inEntityManager(EMF,action)" ); + + try (SessionImplementor session = (SessionImplementor) emf.createEntityManager()) { + log.trace( "EntityManager opened, calling action" ); + action.accept( session ); + log.trace( "called action" ); + } + finally { + log.trace( "EntityManager closed (AutoCloseable)" ); + } + } public static void inTransaction(SessionFactoryImplementor factory, Consumer action) { log.trace( "#inTransaction(factory, action)"); @@ -46,6 +61,67 @@ public class TransactionUtil2 { ); } + public static void inEntityTransaction(EntityManagerFactory factory, Consumer action) { + log.trace( "#inEntityTransaction(factory, action)"); + + inEntityManager( + factory, + session -> { + inEntityTransaction( session, action ); + } + ); + } + + public static void inEntityTransaction(EntityManager entityManager, Consumer action) { + log.trace( "#inTransaction(factory, action)"); + + final EntityTransaction txn = entityManager.getTransaction(); + txn.begin(); + log.trace( "Started transaction" ); + + try { + log.trace( "Calling action in txn" ); + action.accept( entityManager ); + log.trace( "Called action - in txn" ); + + if ( !txn.isActive() ) { + throw new TransactionManagementException( ACTION_COMPLETED_TXN ); + } + } + catch (Exception e) { + // an error happened in the action + if ( ! txn.isActive() ) { + log.warn( ACTION_COMPLETED_TXN, e ); + } + else { + log.trace( "Rolling back transaction due to action error" ); + try { + txn.rollback(); + log.trace( "Rolled back transaction due to action error" ); + } + catch (Exception inner) { + log.trace( "Rolling back transaction due to action error failed; throwing original error" ); + } + } + + throw e; + } + + // action completed with no errors - attempt to commit the transaction allowing + // any RollbackException to propagate. Note that when we get here we know the + // txn is active + + log.trace( "Committing transaction after successful action execution" ); + try { + txn.commit(); + log.trace( "Committing transaction after successful action execution - success" ); + } + catch (Exception e) { + log.trace( "Committing transaction after successful action execution - failure" ); + throw e; + } + } + public static void inTransaction(SessionImplementor session, Consumer action) { log.trace( "inTransaction(session,action)" );