Skip to content

Commit 49f3a6b

Browse files
committed
HibernateTransactionManager supports result access after completion
Issue: SPR-12349
1 parent b16048b commit 49f3a6b

File tree

2 files changed

+119
-2
lines changed

2 files changed

+119
-2
lines changed

spring-orm-hibernate4/src/main/java/org/springframework/orm/hibernate4/HibernateTransactionManager.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.orm.hibernate4;
1818

1919
import java.sql.Connection;
20+
import java.sql.ResultSet;
2021
import javax.sql.DataSource;
2122

2223
import org.hibernate.ConnectionReleaseMode;
@@ -117,6 +118,8 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana
117118

118119
private boolean prepareConnection = true;
119120

121+
private boolean allowResultAccessAfterCompletion = false;
122+
120123
private boolean hibernateManagedSession = false;
121124

122125
private Object entityInterceptor;
@@ -229,6 +232,21 @@ public void setPrepareConnection(boolean prepareConnection) {
229232
this.prepareConnection = prepareConnection;
230233
}
231234

235+
/**
236+
* Set whether to allow result access after completion, typically via Hibernate's
237+
* ScrollableResults mechanism.
238+
* <p>Default is "false". Turning this flag on enforces over-commit holdability on the
239+
* underlying JDBC Connection (if {@link #prepareConnection "prepareConnection"} is on)
240+
* and skips the disconnect-on-completion step.
241+
* @since 4.1.2
242+
* @see java.sql.Connection#setHoldability
243+
* @see ResultSet#HOLD_CURSORS_OVER_COMMIT
244+
* @see #disconnectOnCompletion(Session)
245+
*/
246+
public void setAllowResultAccessAfterCompletion(boolean allowResultAccessAfterCompletion) {
247+
this.allowResultAccessAfterCompletion = allowResultAccessAfterCompletion;
248+
}
249+
232250
/**
233251
* Set whether to operate on a Hibernate-managed Session instead of a
234252
* Spring-managed Session, that is, whether to obtain the Session through
@@ -432,6 +450,13 @@ protected void doBegin(Object transaction, TransactionDefinition definition) {
432450
Connection con = ((SessionImplementor) session).connection();
433451
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
434452
txObject.setPreviousIsolationLevel(previousIsolationLevel);
453+
if (this.allowResultAccessAfterCompletion && !txObject.isNewSession()) {
454+
int currentHoldability = con.getHoldability();
455+
if (currentHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) {
456+
txObject.setPreviousHoldability(currentHoldability);
457+
con.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);
458+
}
459+
}
435460
}
436461
else {
437462
// Not allowed to change the transaction settings of the JDBC Connection.
@@ -625,11 +650,18 @@ protected void doCleanupAfterCompletion(Object transaction) {
625650
// Else, we need to rely on the connection pool to perform proper cleanup.
626651
try {
627652
Connection con = ((SessionImplementor) session).connection();
653+
Integer previousHoldability = txObject.getPreviousHoldability();
654+
if (previousHoldability != null) {
655+
con.setHoldability(previousHoldability);
656+
}
628657
DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
629658
}
630659
catch (HibernateException ex) {
631660
logger.debug("Could not access JDBC Connection of Hibernate Session", ex);
632661
}
662+
catch (Throwable ex) {
663+
logger.debug("Could not reset JDBC Connection after transaction", ex);
664+
}
633665
}
634666

635667
if (txObject.isNewSession()) {
@@ -645,13 +677,26 @@ protected void doCleanupAfterCompletion(Object transaction) {
645677
if (txObject.getSessionHolder().getPreviousFlushMode() != null) {
646678
session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode());
647679
}
648-
if (!this.hibernateManagedSession) {
649-
session.disconnect();
680+
if (!this.allowResultAccessAfterCompletion && !this.hibernateManagedSession) {
681+
disconnectOnCompletion(session);
650682
}
651683
}
652684
txObject.getSessionHolder().clear();
653685
}
654686

687+
/**
688+
* Disconnect a pre-existing Hibernate Session on transaction completion,
689+
* returning its database connection but preserving its entity state.
690+
* <p>The default implementation simply calls {@link Session#disconnect()}.
691+
* Subclasses may override this with a no-op or with fine-tuned disconnection logic.
692+
* @param session the Hibernate Session to disconnect
693+
* @since 4.1.2
694+
* @see org.hibernate.Session#disconnect()
695+
*/
696+
protected void disconnectOnCompletion(Session session) {
697+
session.disconnect();
698+
}
699+
655700
/**
656701
* Return whether the given Hibernate Session will always hold the same
657702
* JDBC Connection. This is used to check whether the transaction manager
@@ -698,6 +743,8 @@ private class HibernateTransactionObject extends JdbcTransactionObjectSupport {
698743

699744
private boolean newSession;
700745

746+
private Integer previousHoldability;
747+
701748
public void setSession(Session session) {
702749
this.sessionHolder = new SessionHolder(session);
703750
this.newSessionHolder = true;
@@ -728,6 +775,14 @@ public boolean isNewSession() {
728775
return this.newSession;
729776
}
730777

778+
public void setPreviousHoldability(Integer previousHoldability) {
779+
this.previousHoldability = previousHoldability;
780+
}
781+
782+
public Integer getPreviousHoldability() {
783+
return this.previousHoldability;
784+
}
785+
731786
public boolean hasSpringManagedTransaction() {
732787
return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null);
733788
}

spring-orm-hibernate4/src/test/java/org/springframework/orm/hibernate4/HibernateTransactionManagerTests.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.sql.Connection;
2020
import java.sql.DatabaseMetaData;
21+
import java.sql.ResultSet;
2122
import java.sql.SQLException;
2223
import java.sql.Savepoint;
2324
import java.util.ArrayList;
@@ -624,6 +625,7 @@ public void testTransactionCommitWithEntityInterceptor() throws Exception {
624625

625626
HibernateTransactionManager tm = new HibernateTransactionManager(sf);
626627
tm.setEntityInterceptor(entityInterceptor);
628+
tm.setAllowResultAccessAfterCompletion(true);
627629
TransactionTemplate tt = new TransactionTemplate(tm);
628630
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
629631
assertTrue("Hasn't thread session", !TransactionSynchronizationManager.hasResource(sf));
@@ -843,6 +845,66 @@ public Object doInTransaction(TransactionStatus status) {
843845
verify(session).disconnect();
844846
}
845847

848+
@Test
849+
public void testTransactionCommitWithPreBoundAndResultAccessAfterCommit() throws Exception {
850+
final DataSource ds = mock(DataSource.class);
851+
Connection con = mock(Connection.class);
852+
final SessionFactory sf = mock(SessionFactory.class);
853+
final ImplementingSession session = mock(ImplementingSession.class);
854+
Transaction tx = mock(Transaction.class);
855+
856+
given(session.beginTransaction()).willReturn(tx);
857+
given(session.isOpen()).willReturn(true);
858+
given(session.getFlushMode()).willReturn(FlushMode.MANUAL);
859+
given(session.connection()).willReturn(con);
860+
given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED);
861+
given(con.getHoldability()).willReturn(ResultSet.CLOSE_CURSORS_AT_COMMIT);
862+
given(session.isConnected()).willReturn(true);
863+
864+
HibernateTransactionManager tm = new HibernateTransactionManager();
865+
tm.setSessionFactory(sf);
866+
tm.setDataSource(ds);
867+
tm.setAllowResultAccessAfterCompletion(true);
868+
TransactionTemplate tt = new TransactionTemplate(tm);
869+
tt.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
870+
final List l = new ArrayList();
871+
l.add("test");
872+
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
873+
assertTrue("JTA synchronizations not active", !TransactionSynchronizationManager.isSynchronizationActive());
874+
TransactionSynchronizationManager.bindResource(sf, new SessionHolder(session));
875+
assertTrue("Has thread session", TransactionSynchronizationManager.hasResource(sf));
876+
877+
Object result = tt.execute(new TransactionCallback() {
878+
@Override
879+
public Object doInTransaction(TransactionStatus status) {
880+
assertTrue("Has thread session", TransactionSynchronizationManager.hasResource(sf));
881+
assertTrue("Has thread connection", TransactionSynchronizationManager.hasResource(ds));
882+
SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sf);
883+
assertTrue("Has thread transaction", sessionHolder.getTransaction() != null);
884+
Session sess = ((SessionHolder) TransactionSynchronizationManager.getResource(sf)).getSession();
885+
assertEquals(session, sess);
886+
return l;
887+
}
888+
});
889+
assertTrue("Correct result list", result == l);
890+
891+
assertTrue("Has thread session", TransactionSynchronizationManager.hasResource(sf));
892+
SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sf);
893+
assertTrue("Hasn't thread transaction", sessionHolder.getTransaction() == null);
894+
TransactionSynchronizationManager.unbindResource(sf);
895+
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
896+
assertTrue("JTA synchronizations not active", !TransactionSynchronizationManager.isSynchronizationActive());
897+
898+
InOrder ordered = inOrder(session, con);
899+
ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
900+
ordered.verify(con).setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);
901+
ordered.verify(session).setFlushMode(FlushMode.AUTO);
902+
ordered.verify(con).setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT);
903+
ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
904+
ordered.verify(session).setFlushMode(FlushMode.MANUAL);
905+
verify(tx).commit();
906+
}
907+
846908
@Test
847909
public void testTransactionRollbackWithPreBound() throws Exception {
848910
final DataSource ds = mock(DataSource.class);

0 commit comments

Comments
 (0)