Trouble using ScrollableResults-backed Stream as return type in Spring MVC
JavaHibernateJava 8Spring OrmOpen Session-in-ViewJava Problem Overview
Spring issue with a target fix version of 4.1.2.
Important note: this has been accepted as aMy goal is to achieve O(1) space complexity when generating an HTTP response from Hibernate's ScrollableResults
. I want to keep the standard mechanism where a MessageConverter
is dispatched to handle an object returned from a @Controller
. I have set up the following:
MappingJackson2HttpMessageConverter
enriched with aJsonSerializer
which handles a Java 8Stream
;- a custom
ScrollableResultSpliterator
needed to wrapScrollableResults
into aStream
; OpenSessionInViewInterceptor
needed to keep the Hibernate session open within theMessageConverter
;- set
hibernate.connection.release_mode
toON_CLOSE
; - ensure that the JDBC connection has the necessary ResultSet holdability:
con.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT)
.
Additionally, I need a database which supports that kind of holdability. PostgreSQL is such a database and I have no trouble with this.
The final stumbling point I have encountered is the policy used by HibernateTransactionManager
on transaction commit: unless the underlying session is "Hibernate-managed", it will disconnect()
it, closing my cursor along with everything else. Such a policy is useful in some special scenarios, specifically "conversation-scoped sessions", which are far removed from my requirements.
I have managed to work around this with a bad hack: I had to override the offending method with a method which is effectively a copy-paste of the original except for the removed disconnect()
call, but it must resort to reflection to access private API.
public class NoDisconnectHibernateTransactionManager extends HibernateTransactionManager
{
private static final Logger logger = LoggerFactory.getLogger(NoDisconnectHibernateTransactionManager.class);
public NoDisconnectHibernateTransactionManager(SessionFactory sf) { super(sf); }
@Override
protected void doCleanupAfterCompletion(Object transaction) {
final JdbcTransactionObjectSupport txObject = (JdbcTransactionObjectSupport) transaction;
final Class<?> c = txObject.getClass();
try {
// Remove the session holder from the thread.
if ((Boolean)jailBreak(c.getMethod("isNewSessionHolder")).invoke(txObject))
TransactionSynchronizationManager.unbindResource(getSessionFactory());
// Remove the JDBC connection holder from the thread, if exposed.
if (getDataSource() != null)
TransactionSynchronizationManager.unbindResource(getDataSource());
final SessionHolder sessionHolder = (SessionHolder)jailBreak(c.getMethod("getSessionHolder")).invoke(txObject);
final Session session = sessionHolder.getSession();
if ((Boolean)jailBreak(HibernateTransactionManager.class.getDeclaredField("prepareConnection")).get(this)
&& session.isConnected() && isSameConnectionForEntireSession(session))
{
// We're running with connection release mode "on_close": We're able to reset
// the isolation level and/or read-only flag of the JDBC Connection here.
// Else, we need to rely on the connection pool to perform proper cleanup.
try {
final Connection con = ((SessionImplementor) session).connection();
DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
}
catch (HibernateException ex) {
logger.debug("Could not access JDBC Connection of Hibernate Session", ex);
}
}
if ((Boolean)jailBreak(c.getMethod("isNewSession")).invoke(txObject)) {
logger.debug("Closing Hibernate Session [{}] after transaction", session);
SessionFactoryUtils.closeSession(session);
}
else {
logger.debug("Not closing pre-bound Hibernate Session [{}] after transaction", session);
if (sessionHolder.getPreviousFlushMode() != null)
session.setFlushMode(sessionHolder.getPreviousFlushMode());
}
sessionHolder.clear();
}
catch (ReflectiveOperationException e) { throw new RuntimeException(e); }
}
static <T extends AccessibleObject> T jailBreak(T o) { o.setAccessible(true); return o; }
}
Since I regard my approach as the "right way" to generate ResultSet-backed responses, and since the Streams API makes this approach very convenient, I would like to solve this in a supported way.
Is there a way to get the same behavior without my hack? If not, would this be a good thing to request via Spring's Jira?
Java Solutions
Solution 1 - Java
Cleaning up. As Marko Topolnik had said here
> Yes, I missed this part that the holdability setting is only applied when encountering a pre-existing session. This means that my "idea" how it could be done is already the way it is done. It also means that my comment about failures doesn't apply: you either want holdability and skipping the session disconnection — or you don't need either. So if you can't get holdability, there is no reason not to disconnect the session at commit, therefore there's no reason to activate the "allowResultSetAccessAfterCompletion" in that case.