Important note: this has been accepted as a Spring issue with a target fix version of 4.1.2.
My 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?
set(null)
afternext()
. Still the memory footprint is pretty much the same. CoW is a fair assessment but I wonder if anyone would actually do it as it'd require to explicitly keep the row version in the resulset. Later I will look into the problem to see if I can come up with anything better. – CardonArrayList
such that it has this behavior. Its iterator is a private class and has access to private state. It is clearly designed against extension. So getting this in practice would require quite a lot of cumbersome work (although I guess a naive Iterator implementation with slightly less efficiency would be easy). – TransmittanceHibernateTransactionObject
should be a static class - very reasonable request imo. Then it should be protected not private andHibernateTransactionManager
should use a protected factory method to create the instance. Very reasonable improvements, and trivial - I guess you should be able to push it. – CardonHibernateTransactionObject
which would isolate this concern. Then I would just override your suggested factory to return my slight modification ofHibernateTransactionObject
. – TransmittanceHibernate
, then a little extension of Spring'sJdbcTemplate
would be easy. Let me know if you want to knwo more detail. – OshinskiJdbcTemplate
orNamedParameterJdbcTemplate
depending on which API you wanted to havestream
. So far Spring doesn't support that in theirJdbcTemplate
or its inheritance – Oshinski