HibernateTransactionManager supports result access after completion

Issue: SPR-12349
This commit is contained in:
Juergen Hoeller 2014-10-23 16:40:09 +02:00
parent b16048b061
commit 49f3a6beff
2 changed files with 119 additions and 2 deletions

View File

@ -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.
* <p>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.
* <p>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);
}

View File

@ -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);