Hi Mark,
we had the same problem with Hibernate. I solved it with a "flow" wrapper. This wrapper wraps the flow interpreter of your choice (tested with javaflow a lot, simple tests with _javascript_, but should not give big problems), opens a new session when the flow is first accessed (map:call function), detaches the session from the DB when the flow sends a page, then retaches the same session when the continuation is invoked (map:call continuation). It schedules a task to recollect and close sessions connected to continuations which has been marked as invalid.

This magically gives you a coherent hibernate session inside your flow, but it has some drawbacks :
- Sessions are left there, open (but disconnected from the database, so the DB collection is free) for all the time it takes for the continuation tree to be marked invalid, so potentially much more than needed, but there isn't currently a notion of "flow finished", so i don't think there can be any other way. This will increase memory usage.
- It's written for a hibernate + spring configuration, since it uses spring transaction manager to set the session as "current session" while executing the flow.
- Since the DB connection is detached, it forces a "flush never" to avoid spring/hibernate to flush the connection and thus persist object while they are, for example, being edited in a form or manipulated by the flow. This means you must manually flush the connection (or the spring hibernate template in DAOs, or whatever else) to persist your changes where needed (you can access the session inside the flow with request.getAttribute("HIBERNATE_SESSION") to do whatever you want with it). This is again needed since there is no way of knowing automatically when it is ok to flush the session and when we are just in an intermediate sendpageandwait.
- Since the DB connection is detached, this can create potential problems in database transaction for databases which does not support transactions spanning different connections (which one does?) when not using another external transaction system.

But also brings many advantages :
- You can use lazy loading everywhere, since the session will be there as long as the continuation will be there.
- No more worries about objects being persisted in the middle of a flow-form interaction (thus causing hibernate exceptions, or even worse data corruption)
- Hibernate persistence will now be "horizontal and transparent", at least in your flow (DAO, or backend services, will always need to know something about hibernate)
- No problems of merging, stale objects, duplicate objects ... that would arise with other tecniques (object detach, retach; multiple sessions etc..)

We are currently developing a lot of stuff using cocoon + hibernate + spring with this component.

You can find javadoc in the main HibernateFlowAdapter2 class, explaining how to use it, feel free to ask everything you don't understand.

Unfortunately this code will not enter in the cocoon repository cause it would include dependencies on hibernate, so i think we can use this thread to keep it up to date in case you improve it.


Hope it helps!
Simone

Mark H wrote:

What is the best way to handle hibernate sessions in flow? At the moment I’m using a servlet filter to close sessions after each request but this makes it awkward when dealing with objects that span a number of requests but are within one flow function (I’m using flowscript). I could have the session span the flowscript function but if the user never finishes the flow the session will never be closed.

 

Mark H

 

--
Simone Gianni
/*
 * Copyright 2005 Pro-netics S.r.l.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.pnetx.pulse.forms;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.service.ServiceSelector;
import org.apache.avalon.framework.service.Serviceable;
import org.apache.cocoon.components.ContextHelper;
import org.apache.cocoon.components.flow.AbstractInterpreter;
import org.apache.cocoon.components.flow.ContinuationsDisposer;
import org.apache.cocoon.components.flow.ContinuationsManager;
import org.apache.cocoon.components.flow.FlowHelper;
import org.apache.cocoon.components.flow.Interpreter;
import org.apache.cocoon.components.flow.WebContinuation;
import org.apache.cocoon.components.thread.RunnableManager;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Redirector;
import org.apache.cocoon.environment.Request;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.springframework.context.ApplicationContext;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.context.WebApplicationContext;

/**
 * Virtual interpreter to adapt any other flow interpreter to hibernate.
 * <h2>Description</h2>
 * <p>
 * One common problem with hibernate and cocoon is the detachment of the
 * persistent objects, and what follows (LazyInitialization exceptions, stale
 * objects etc..). This happens because hibernate Sessions are short lived
 * objects, and common web applications open a session, use it to work on an
 * object, then persist it, and carry on passing object ids in hidden fields or
 * similar stuff.
 * </p>
 * <p>
 * With continuation, it's very handy to use our persistent objects directly,
 * but the usual DAO pattern combined with Spring is to
 * close the Session as soon as possible, which often means before our flow
 * processing ends and before the view layer have the opportunity to access the
 * persistent objects (see OpenSessionInViewFilter/Interceptor for this).
 * </p>
 * <p>
 * What this adapter does is quite simple :
 * <ul>
 * <li>Opens a new Transaction for every continuation call.</li>
 * <li>Inits the TransactionSynchronizationManager with this session, so that
 * DAOs invoked by the flow wil use it</li>
 * <li>When the flow generates a new continuation (sendPageAndWait or
 * form.show), it saves the current hibernate session associated to the root
 * continuation.</li>
 * <li>When the subseguent continuation is invoked, it will still use the same session.
 * </li>
 * <li>Schedules a task that will check for continuations disposal and close the
 * hibernate session-</li>
 * </ul>
 * <h2>Usage</h2>
 * <p>
 * In cocoon.xconf configure both the real interpreter and this interpreter in
 * this way :
 * </p>
 * 
 * <pre>
 *         &lt;flow-interpreters default=&quot;javascript&quot; logger=&quot;flow&quot;&gt;
 *           &lt;component-instance class=&quot;org.apache.cocoon.components.flow.java.JavaInterpreter&quot; name=&quot;java&quot;/&gt;
 *           &lt;component-instance class=&quot;com.pnetx.pulse.forms.HibernateFlowAdapter2&quot; name=&quot;javahibernate&quot; adapt=&quot;java&quot;/&gt;
 *         &lt;/flow-interpreters&gt;
 * </pre>
 * 
 * <p>
 * Then use the javahibernate language when loading your flows, and you will
 * have the same hibernate session for all the flow execution.
 * </p>
 * <p>
 * You can optionally specify a check-period in milliseconds for the continuations disposed
 * check, default is 3 minutes.
 * </p>
 * <p>
 * Special thanks to Luca Masini for giving a good example techiques used in this class 
 * <a href="http://opensource2.atlassian.com/confluence/spring/pages/viewpage.action?pageId=1447";>in his filter</a>,
 * and to Gianugo Rabellino for pointing me to that.
 * </p>
 * @author Simone Gianni
 * @version $Id: HibernateFlowAdapter.java 927 2005-12-22 16:25:24 +0100 (Thu,
 *                    22 Dec 2005) u.cei $
 */
public class HibernateFlowAdapter2 extends AbstractInterpreter implements Interpreter, Serviceable, Configurable {

    private boolean detach = true;
    
    private String sessionFactoryBeanName = "sessionFactory";

    private AbstractInterpreter concrete;

    private Map wrappers = new HashMap();

    private String interpreterID;

    private ContinuationsManager continuationsMgr;

    private ServiceManager manager;

    private SessionFactory mSessionFactory;

    public HibernateFlowAdapter2() throws Exception {
        super();
    }

    protected void setup(WebContinuation kont) {
        // Always remove existing session if any
        if (isSessionBoundToThread()) {
            resetThreadSession();
        }        
        // First execution of a function, simply create a session 
        if (kont == null) {
            Session session = getSession();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("Creating new hibernate session  (first execution, session " + session.hashCode() + ")");
            }            
            return;
        }
        // Else find the "root" continuation
        WebContinuation root = kont.getContinuation(Integer.MAX_VALUE);
        // Look if there is a session wrapper for this tree
        SessionWrapper wrapper = (SessionWrapper) this.wrappers.get(root);
        // If there is a wrapper and has an open session then use it, else
        // create a new one and save it
        if (wrapper != null && wrapper.isSessionOpened()) {
            Session session = wrapper.getSession();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("Restoring existing hibernate session for continuation " + kont.getId() + " (root " + root.getId() + ", session " + session.hashCode() + ")");
            }
            restoreSession(session);
        } else {
            if (getLogger().isDebugEnabled()) {
                getLogger().warn("Creating new hibernate session for continuation " + kont.getId() + " (" + (wrapper != null ? ("previous session" + wrapper.getSession().hashCode()) : "no wrapper found") + ")");
            }
            wrapper = new SessionWrapper(getSession());
            this.wrappers.put(kont, wrapper);
        }
    }

    protected void tearDown(WebContinuation kont) {
        WebContinuation root = kont.getContinuation(Integer.MAX_VALUE);
        SessionWrapper wrapper = (SessionWrapper) this.wrappers.get(root);
        // If there is a wrapper reset it's state
        if (wrapper != null) {
            wrapper.resetIsAllocated();
        } else {
            // Create a new wrapper with current session
            Session session = getCurrentSession();
            if (session != null) {
	            if (getLogger().isDebugEnabled()) {
	                getLogger().debug("Saving the hibernate session for continuation " + kont.getId() + " (wrapper not found)");
	            }
	            wrapper = new SessionWrapper(session);
	            wrapper.resetIsAllocated();
	            this.wrappers.put(kont, wrapper);
            } else {
                getLogger().warn("No hibernate session found, not in wrapper nor in thread, for continuation " + kont.getId());
            }
        }
        // Commit the transaction
        doTransactionCommit();
        // Remove the transaction from the current thread
        resetThreadSession();
        // Find the root continuation and it's wrapper
    }

    protected void disposeContinuation(WebContinuation kont) {
        // Kont is already a root continuation
        WebContinuation root = kont.getContinuation(Integer.MAX_VALUE);
        // Look if there is a session wrapper for this tree
        SessionWrapper wrapper = (SessionWrapper) this.wrappers.get(root);

        if (wrapper != null) {
            Session session = wrapper.getSession();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("Closing hibernate session for continuation " + root.getId() + " (session " + session.hashCode() + ")");
            }
            // Close the session
            restoreSession(session);
            try {
                doTransactionCommit();
            } catch (HibernateException e) {
                getLogger().error("error during Hibernate COMMIT, SKIPPING", e);
                doTransactionRollback();
            }
            doCloseSession();
        }
    }

    protected void disposeContinuations() {
        for (Iterator iter = this.wrappers.keySet().iterator(); iter.hasNext();) {
            WebContinuation root = (WebContinuation) iter.next();
            if (root.disposed()) {
                try {
                    disposeContinuation(root);
                } catch (Throwable e) {
                    getLogger().error("Error disposing continuation " + root.getId(), e);
                }
                // Remove the wrapper anyway
                iter.remove();
            }
        }
    }

    protected void handleException() {
        doTransactionRollback();
    }

    /**
     * @param funName
     * @param params
     * @param redirector
     * @throws java.lang.Exception
     */
    public void callFunction(String funName, List params, Redirector redirector) throws Exception {
        setup(null);
        sessionInRequest();
        concrete.callFunction(funName, params, redirector);
        try {
            WebContinuation kont = FlowHelper.getWebContinuation(ContextHelper.getObjectModel(this.avalonContext));
            tearDown(kont);
        } catch (Exception e) {
            handleException();
            throw e;
        }
    }

    /**
     * @param uri
     * @param bizData
     * @param continuation
     * @param redirector
     * @throws java.lang.Exception
     */
    public void forwardTo(String uri, Object bizData, WebContinuation continuation, Redirector redirector) throws Exception {
        concrete.forwardTo(uri, bizData, continuation, redirector);
    }

    /**
     * @return
     */
    public String getInterpreterID() {
        return this.interpreterID;
    }

    /**
     * @param continuationId
     * @param params
     * @param redirector
     * @throws java.lang.Exception
     */
    public void handleContinuation(String continuationId, List params, Redirector redirector) throws Exception {
        WebContinuation parentwk = continuationsMgr.lookupWebContinuation(continuationId, concrete.getInterpreterID());
        setup(parentwk);
        sessionInRequest();
        try {
            concrete.handleContinuation(continuationId, params, redirector);
            WebContinuation kont = FlowHelper.getWebContinuation(ContextHelper.getObjectModel(this.avalonContext));
            tearDown(kont);
        } catch (Exception e) {
            handleException();
            throw e;
        }
    }

    /**
     * @param interpreterID
     */
    public void setInterpreterID(String interpreterID) {
        concrete.setInterpreterID(interpreterID);
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    public String toString() {
        return "HibernateAdapted " + concrete.toString();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
     */
    public void service(ServiceManager sm) throws ServiceException {
        this.manager = sm;
        this.continuationsMgr = (ContinuationsManager) sm.lookup(ContinuationsManager.ROLE);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration)
     */
    public void configure(Configuration conf) throws ConfigurationException {
        // Get the mandatory adapt attribute
        if (conf.getAttribute("adapt") == null)
            throw new ConfigurationException("The adapt attribute must be valorized with the language name you want to adapt to hibernate");
        String language = conf.getAttribute("adapt");
        this.detach = conf.getAttributeAsBoolean("detach", true);

        try {
            Object selector = manager.lookup(Interpreter.ROLE);
            ServiceSelector interpreterSelector = (ServiceSelector) selector;
            // Obtain the Interpreter instance for this language
            this.concrete = (AbstractInterpreter) interpreterSelector.select(language);
            // Set interpreter ID as URI of the flow node (full sitemap file
            // path)
        } catch (Exception e) {
            throw new ConfigurationException("Cannot load interpreter " + language, e);
        }
        long interval = conf.getChild("check-period", true).getValueAsLong(180000);
        try {
            RunnableManager runnableManager = (RunnableManager) manager.lookup(RunnableManager.ROLE);
            runnableManager.execute(new Runnable() {
                public void run() {
                    disposeContinuations();
                }
            }, interval, interval);
            manager.release(runnableManager);
        } catch (ServiceException e1) {
            throw new ConfigurationException("Cannot start disposer thread, Hibernate sessions would live for ever", e1);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.apache.cocoon.components.flow.AbstractInterpreter#register(java.lang.String)
     */
    public void register(String source) {
        concrete.register(source);
    }

    // Utility methods for hibernate session handling

    protected SessionFactory lookupSessionFactory() {
        if (this.mSessionFactory == null) {
	        if (getLogger().isDebugEnabled()) {
	            getLogger().debug("Using SessionFactory '" + sessionFactoryBeanName + "' for HibernateContinuationsManager");
	        }
	        Map objectModel = ContextHelper.getObjectModel(this.avalonContext);
	        ApplicationContext wac = (ApplicationContext) ObjectModelHelper.getContext(objectModel).getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
	        this.mSessionFactory = (SessionFactory) wac.getBean(sessionFactoryBeanName, SessionFactory.class);
        }
        return this.mSessionFactory;
    }

    protected void restoreSession(Session session) {
        if (isSessionBoundToThread()) {
            resetThreadSession();
        }

        TransactionSynchronizationManager.bindResource(lookupSessionFactory(), new SessionHolder(session));
        TransactionSynchronizationManager.initSynchronization();
        reconnectSession();
        doBeginTransaction();
    }

    protected boolean isSessionBoundToThread() {
        return TransactionSynchronizationManager.hasResource(lookupSessionFactory());
    }

    protected void doCloseSession() throws HibernateException {
        if (isSessionBoundToThread()) {
            getCurrentSession().close();
        }
    }

    protected void resetThreadSession() {
        if (isSessionBoundToThread()) {
            TransactionSynchronizationManager.unbindResource(lookupSessionFactory());
            TransactionSynchronizationManager.clearSynchronization();
        }
    }

    protected void reconnectSession() throws HibernateException {
        Session session = getCurrentSession();
        if (session != null && !session.isConnected()) {
            session.reconnect();
        }
    }

    protected void disconnectSession() throws HibernateException {
        Session session = getCurrentSession();
        if (isSessionBoundToThread() && session != null && session.isConnected() && detach) {
            session.disconnect();
        }
    }

    protected void doBeginTransaction() throws HibernateException {
        SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(lookupSessionFactory());
        if (sessionHolder.getTransaction() == null) {
           // sessionHolder.setTransaction(sessionHolder.getSession().beginTransaction());
        }
    }

    /**
     * @return A new session
     */
    protected Session getSession() {
        if (!isSessionBoundToThread()) {
            SessionHolder sessionHolder = new SessionHolder(lookupSessionFactory().openSession());
            sessionHolder.getSession().setFlushMode(FlushMode.NEVER);
            //sessionHolder.setTransaction(sessionHolder.getSession().beginTransaction());
            TransactionSynchronizationManager.bindResource(lookupSessionFactory(), sessionHolder);
            TransactionSynchronizationManager.initSynchronization();
        }
        reconnectSession();
        return getCurrentSession();
    }
    
    protected Session getCurrentSession() {
        SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(lookupSessionFactory());
        Session session = sessionHolder == null ? null : sessionHolder.getSession();
        if (session != null) {
            // Force the flush NEVER, we don't want to persist temporary object currently subject to form editing.
            //session.setFlushMode(FlushMode.NEVER);
            // Would be nice to do so, but spring just gets mad
        }
        return session;
    }

    protected void doTransactionCommit() throws HibernateException {
        if (getTransaction() != null && !getTransaction().wasCommitted() && !getTransaction().wasRolledBack()) {
            getCurrentSession().setFlushMode(FlushMode.NEVER);
            //getTransaction().commit();
        }
        disconnectSession();
    }

    protected Transaction getTransaction() {
        if (isSessionBoundToThread()) {
            return ((SessionHolder) TransactionSynchronizationManager.getResource(lookupSessionFactory())).getTransaction();
        }
        return null;
    }

    protected void doTransactionRollback() {
        if (getTransaction() != null && !getTransaction().wasCommitted() && !getTransaction().wasRolledBack()) {
            getTransaction().rollback();
        }
        doCloseSession();
    }

    protected void sessionInRequest() {
        Session session = getCurrentSession();
        if (session == null) return;
        Request request = ContextHelper.getRequest(this.avalonContext);
        if (request == null) return;
        request.setAttribute("HIBERNATE_SESSION", session);
    }
    
    private class Disposer implements ContinuationsDisposer {

        /*
         * (non-Javadoc)
         * 
         * @see org.apache.cocoon.components.flow.ContinuationsDisposer#disposeContinuation(org.apache.cocoon.components.flow.WebContinuation)
         */
        public void disposeContinuation(WebContinuation webContinuation) {
            HibernateFlowAdapter2.this.disposeContinuation(webContinuation);
        }
        /*
         * (non-Javadoc)
         * 
         * @see org.apache.cocoon.components.flow.ContinuationsDisposer#disposeContinuation(org.apache.cocoon.components.flow.WebContinuation)
         */
    }
}
/*
 * Created on 18-Jan-2006
 */
package com.pnetx.pulse.forms;

import java.util.ConcurrentModificationException;

import org.hibernate.Session;

/**
 * @author sym
 */
public class SessionWrapper {

    private final transient Session m_Session;

    private boolean m_IsAllocated = false;

    public SessionWrapper(Session session) {
        m_Session = session;
    }

    public Session getSession() {
        try {
            if (m_IsAllocated) {
                throw new ConcurrentModificationException("Hibernate Session is not reentrant");
            }
            return m_Session;
        } finally {
            m_IsAllocated = true;
        }
    }

    public boolean isSessionOpened() {
        return m_Session.isOpen();
    }

    public void resetIsAllocated() {
        m_IsAllocated = false;
    }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to