From 49f3a6beffc744f6457426df1383f90f620eed74 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 Oct 2014 16:40:09 +0200 Subject: [PATCH] HibernateTransactionManager supports result access after completion Issue: SPR-12349 --- .../HibernateTransactionManager.java | 59 +++++++++++++++++- .../HibernateTransactionManagerTests.java | 62 +++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/HibernateTransactionManager.java b/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/HibernateTransactionManager.java index 2ac8dfb7f58..ad527e3867a 100644 --- a/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/HibernateTransactionManager.java +++ b/spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/HibernateTransactionManager.java @@ -17,6 +17,7 @@ package org.springframework.orm.hibernate4; import java.sql.Connection; +import java.sql.ResultSet; import javax.sql.DataSource; import org.hibernate.ConnectionReleaseMode; @@ -117,6 +118,8 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana private boolean prepareConnection = true; + private boolean allowResultAccessAfterCompletion = false; + private boolean hibernateManagedSession = false; private Object entityInterceptor; @@ -229,6 +232,21 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana this.prepareConnection = prepareConnection; } + /** + * Set whether to allow result access after completion, typically via Hibernate's + * ScrollableResults mechanism. + *

Default is "false". Turning this flag on enforces over-commit holdability on the + * underlying JDBC Connection (if {@link #prepareConnection "prepareConnection"} is on) + * and skips the disconnect-on-completion step. + * @since 4.1.2 + * @see java.sql.Connection#setHoldability + * @see ResultSet#HOLD_CURSORS_OVER_COMMIT + * @see #disconnectOnCompletion(Session) + */ + public void setAllowResultAccessAfterCompletion(boolean allowResultAccessAfterCompletion) { + this.allowResultAccessAfterCompletion = allowResultAccessAfterCompletion; + } + /** * Set whether to operate on a Hibernate-managed Session instead of a * Spring-managed Session, that is, whether to obtain the Session through @@ -432,6 +450,13 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana Connection con = ((SessionImplementor) session).connection(); Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); + if (this.allowResultAccessAfterCompletion && !txObject.isNewSession()) { + int currentHoldability = con.getHoldability(); + if (currentHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) { + txObject.setPreviousHoldability(currentHoldability); + con.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT); + } + } } else { // Not allowed to change the transaction settings of the JDBC Connection. @@ -625,11 +650,18 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana // Else, we need to rely on the connection pool to perform proper cleanup. try { Connection con = ((SessionImplementor) session).connection(); + Integer previousHoldability = txObject.getPreviousHoldability(); + if (previousHoldability != null) { + con.setHoldability(previousHoldability); + } DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); } catch (HibernateException ex) { logger.debug("Could not access JDBC Connection of Hibernate Session", ex); } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } } if (txObject.isNewSession()) { @@ -645,13 +677,26 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana if (txObject.getSessionHolder().getPreviousFlushMode() != null) { session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode()); } - if (!this.hibernateManagedSession) { - session.disconnect(); + if (!this.allowResultAccessAfterCompletion && !this.hibernateManagedSession) { + disconnectOnCompletion(session); } } txObject.getSessionHolder().clear(); } + /** + * Disconnect a pre-existing Hibernate Session on transaction completion, + * returning its database connection but preserving its entity state. + *

The default implementation simply calls {@link Session#disconnect()}. + * Subclasses may override this with a no-op or with fine-tuned disconnection logic. + * @param session the Hibernate Session to disconnect + * @since 4.1.2 + * @see org.hibernate.Session#disconnect() + */ + protected void disconnectOnCompletion(Session session) { + session.disconnect(); + } + /** * Return whether the given Hibernate Session will always hold the same * JDBC Connection. This is used to check whether the transaction manager @@ -698,6 +743,8 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana private boolean newSession; + private Integer previousHoldability; + public void setSession(Session session) { this.sessionHolder = new SessionHolder(session); this.newSessionHolder = true; @@ -728,6 +775,14 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana return this.newSession; } + public void setPreviousHoldability(Integer previousHoldability) { + this.previousHoldability = previousHoldability; + } + + public Integer getPreviousHoldability() { + return this.previousHoldability; + } + public boolean hasSpringManagedTransaction() { return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null); } diff --git a/spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java b/spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java index aaca8e9fee4..590db0654c0 100644 --- a/spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java +++ b/spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java @@ -18,6 +18,7 @@ package org.springframework.orm.hibernate4; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Savepoint; import java.util.ArrayList; @@ -624,6 +625,7 @@ public class HibernateTransactionManagerTests { HibernateTransactionManager tm = new HibernateTransactionManager(sf); tm.setEntityInterceptor(entityInterceptor); + tm.setAllowResultAccessAfterCompletion(true); TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertTrue("Hasn't thread session", !TransactionSynchronizationManager.hasResource(sf)); @@ -843,6 +845,66 @@ public class HibernateTransactionManagerTests { verify(session).disconnect(); } + @Test + public void testTransactionCommitWithPreBoundAndResultAccessAfterCommit() throws Exception { + final DataSource ds = mock(DataSource.class); + Connection con = mock(Connection.class); + final SessionFactory sf = mock(SessionFactory.class); + final ImplementingSession session = mock(ImplementingSession.class); + Transaction tx = mock(Transaction.class); + + given(session.beginTransaction()).willReturn(tx); + given(session.isOpen()).willReturn(true); + given(session.getFlushMode()).willReturn(FlushMode.MANUAL); + given(session.connection()).willReturn(con); + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + given(con.getHoldability()).willReturn(ResultSet.CLOSE_CURSORS_AT_COMMIT); + given(session.isConnected()).willReturn(true); + + HibernateTransactionManager tm = new HibernateTransactionManager(); + tm.setSessionFactory(sf); + tm.setDataSource(ds); + tm.setAllowResultAccessAfterCompletion(true); + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + final List l = new ArrayList(); + l.add("test"); + assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds)); + assertTrue("JTA synchronizations not active", !TransactionSynchronizationManager.isSynchronizationActive()); + TransactionSynchronizationManager.bindResource(sf, new SessionHolder(session)); + assertTrue("Has thread session", TransactionSynchronizationManager.hasResource(sf)); + + Object result = tt.execute(new TransactionCallback() { + @Override + public Object doInTransaction(TransactionStatus status) { + assertTrue("Has thread session", TransactionSynchronizationManager.hasResource(sf)); + assertTrue("Has thread connection", TransactionSynchronizationManager.hasResource(ds)); + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sf); + assertTrue("Has thread transaction", sessionHolder.getTransaction() != null); + Session sess = ((SessionHolder) TransactionSynchronizationManager.getResource(sf)).getSession(); + assertEquals(session, sess); + return l; + } + }); + assertTrue("Correct result list", result == l); + + assertTrue("Has thread session", TransactionSynchronizationManager.hasResource(sf)); + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sf); + assertTrue("Hasn't thread transaction", sessionHolder.getTransaction() == null); + TransactionSynchronizationManager.unbindResource(sf); + assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds)); + assertTrue("JTA synchronizations not active", !TransactionSynchronizationManager.isSynchronizationActive()); + + InOrder ordered = inOrder(session, con); + ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + ordered.verify(con).setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT); + ordered.verify(session).setFlushMode(FlushMode.AUTO); + ordered.verify(con).setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT); + ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + ordered.verify(session).setFlushMode(FlushMode.MANUAL); + verify(tx).commit(); + } + @Test public void testTransactionRollbackWithPreBound() throws Exception { final DataSource ds = mock(DataSource.class);