Member Menu
 
 Monthly JBoss newsletter:
 
Java Persistence with Hibernate
CaveatEmptor

Defensive session handling

Pattern for safe Session usage

This is a counter-pattern for the ThreadLocal session approach. Using a piece of advice from Alan Cox from his site ( http://www.pingwales.co.uk/software/cox-on-better-software_1.html item "Good interfaces"), we got the idea of making a single wrapper in which all session based work should be performed:

   // the code is a method in the class SessionManager, that will be detailed further-on.

    public void runSession(DBAction dbAction) {
        Session session = null;
        try {
            session = newSession();
            dbAction.run(session);
            commitSession(session);
        }
        catch (HibernateException hibernateException ) {
            throw new DatabaseException(hibernateException);
        }
        finally {
            rollbackSessionWhenOpen(session);
        }
    }

and

// Arange imports using your IDE.

/**
 * This interface must be anonymously implemented by methods
 * wishing to make use of the SessionManager.runSession() method.
 *
 * @author Erik Visser, Chess-iT
 * @version $Revision: 1.1 $
 */
public interface DBAction {
    
    /**
     * This method performs whatever work must be done in the context of a single
     * database session. 
     * The session will be commited if no exceptions occur and rolled-back if they do.
     * Hibernate Exceptions will be wrapped with DatabaseExceptions.
     * 
     * @param session the newly created db session.
     * @throws HibernateException a Hibernate problem
     */
    public void run(Session session) throws HibernateException;

}

Using this approach, a piece of your code would look like this:

    sessionManager.runSession(new DBAction() {
            public void run(Session session) throws HibernateException {
            
                // use your session here.
            
            }
    });

Now this approach has a number of advances:

  • You cannot forget to commit or rollback your session.
  • You can choose how to deal with exceptions and convert them to more appropriate exceptions (runtime in our case) in a single place.
  • It becomes harder to pass 'live' objects out of the session-context. This is especially useful if you are using lazy colletions or proxy objects.
  • You can easily start a second session in the same thread that is not related to your first session. It might not be recommended because objects 'live' in only one of those sessions, but in practice there are always situations where you still want to do it.
  • You have a session object to pass around, so that you can instantly see in each method that it is running in a session context *.

The disadvantages of this approach are:

  • You cannot throw checked (non-runtime) Exceptions out of a session block. This is actually quite annoying.
  • Code looks awkward, and you will need to brush up your anonymous inner class expertise. This might be especially confusing for inexperienced developers.
  • It becomes harder to pass back anything from the inner class, as all parameters that you can use, must be final. When you work your way around that problem (one way or another), the code will enevitably become less readable.
  • The pattern does not always apply. This is the case when you must return control to an external system (such as in the case when you are writing a jsp-filter), and will regain control back later from that external system.

On average, we find that the advantages heavily outweigh the disadvantages, since most of what you want to do ends when you commit your session.

Here then is the complete SessionManager class:

// arrange imports using your IDE.

/**
*
* @author Erik Visser (Chess-iT)
* @version $Revision: 1.1 $
*/
public class SessionManager {

   private static final Log logger = LogFactory.getLog(SessionManager.class);


   /**
    * The SessionFactory that creates sessions.
    */
   private final SessionFactory sessionFactory;
   
   private Map transactions = new HashMap();


   /**
    * Create a new ThreadSessionManager with the supplied sessionFactory that is 
    * used to generate sessions.
    *
    * @param sessionFactory The sessionFactory to use
    */
   public SessionManager(SessionFactory sessionFactory) {
       this.sessionFactory = sessionFactory;
   }
   
   
   public void runSession(DBAction dbAction) {
       Session session = null;
       try {
           session = newSession();
           dbAction.run(session);
           commitSession(session);
       }
       catch (HibernateException hibernateException ) {
           throw new DatabaseException(hibernateException);
       }
       finally {
           rollbackSessionWhenOpen(session);
       }
   }
   

   /**
    * Method commitSession first flushes and commits all persistency actions,
    * closes the session. 
    * Tries to roll back the session if commit fails.
    */
   public void commitSession(Session session) {
       Transaction tx = (Transaction) transactions.get(session);

       if (tx == null) {
           throw new DatabaseException("Tried to commit a session that" + 
               " was not running a transaction.");
       }

       try {
           tx.commit();
       }
       catch (Exception e) {
           try {
                tx.rollback();
           }
           catch (Exception er) {
               logger.warn("no rollback possible after unsuccesfull commit", er);
           }
              throw new DatabaseException(e);
       }
       finally {
           try {
               session.close();
           }
           catch (Exception e) {
               throw new DatabaseException(e);
           }
           finally {
               transactions.remove(session);
               logger.debug("commited and closed current session.");
           }
       }
   }

   /**
    * Method rollbackSession rolls back all persistency actions and closes 
    * the current session for the current thread, and removes
    * its association with this thread. Throws a ParkmanDatabaseException 
    * if no session is available, or something else went
    * wrong.
    */
   private void rollbackSession(Session session) {
       Transaction tx = (Transaction) transactions.get(session);

       if (tx == null) {
           String errorMsg = "Tried to rollback the session" +
               " when there was no transaction started for this session.";
        logger.info(errorMsg);
        return;        
       }

       try {
           tx.rollback();
       }
       catch (Exception e) {
           throw new DatabaseException(e);
       }
       finally {
           try {
               session.close();
           }
           catch (Exception e) {
               throw new DatabaseException(e);
           }
           finally {
        transactions.remove(session);
               logger.debug("rolled back and closed current session.");
           }
       }
   }

   /**
    * Method newSession starts a new session.
    *
    * @return Session the new session.
    */
   public Session newSession() {
       if (sessionFactory == null || transactions == null ) {
             logger.fatal("unable to create new session");
           throw new DatabaseException("Tried to open a session" + 
               " without having a SessionFactory.");
       }

       try {
           Session session = sessionFactory.openSession();
        Transaction tx = session.beginTransaction();

        transactions.put(session, tx);

        logger.debug("started new session.");

           return session;
       }
       catch (Exception e) {
           throw new DatabaseException(e);
       }

   }

   /**
    * This method rolls back the current session if it is open.
    * This call is typically done in a finally block.
    */
   public void rollbackSessionWhenOpen(Session session) {

       if (session != null && session.isOpen()) {
           rollbackSession(session);
       }
   }
}

The DatabaseException is a roll-your-own RuntimeException that indicates an unexpected and not "I do not wish to recover from it" Hibernate Exception:

public class DatabaseException extends RuntimeException {
....

Rationale:

Having used the ThreadLocal Session pattern for some time now, we found that this pattern was the cause of a large number of problems in our code. Most had to do with the usage of 'live' objects from outside the session that they live in. This is problematic because we use lazy collections and proxy object a lot.

Another problem was that it took us a while to figure out how to deal with closing sessions and rethrowing exceptions. In the end, whe came up with this piece of software that basically was repeated ad infinitum:

    threadSession = <.. our ThreadSession implementation ..>
        try {
            threadSession.newSession();
            
            // actual 'in session' code here
            
            threadSession.commitSession();
        }
        catch (HibernateException hibernateException ) {
            throw new DatabaseException(hibernateException);
        }
        finally {
            threadSession.rollbackSessionWhenOpen();
        }

Where the threadSession object was our implementation of the ThreadLocal session pattern. Especially the last finally is crucial to stop your sessions from leaking.

However, there are two problems with this approach: first you have to repeat a lot of code, and secondly, it is still easy to pass live objects out of the session. So in the end we came up with the approach described above.

Cheers,

Erik Visser


  NEW COMMENT

Do the DBAction implementations have to be anonymous? 20 Jan 2005, 09:55 BruceS
Hi,

I am a java newbie, and my knowledge of "anonymous inner classes" is 
severely lacking...

So do the DBAction implementations HAVE to be anonymous?....and if so 
why?

If so, am I correct is guessing that it have something to do with 
threading/access control to object instances?

Bruce
 
Anon inner classes 28 Jan 2005, 14:47 polyzois@bredband.net
No they don't, it's just easier that way
 
Re: Do the DBAction implementations have to be anonymous? 28 Jan 2005, 14:49 polyzois@bredband.net
No they don't, it's just easier that way
 
Note 30 Jan 2005, 12:30 christian
Some points:

1. Exceptions in Hibernate3 are now all RuntimeException.

2. Problems with committing and closing a Session can be easily solved by 
using an Interceptor approach (either roll your own SessionFilter, 
proprietary EJB Interceptor, or use the built-in support in JBoss AS).

3. Usually all problems with lazy loading and/or clear boundaries for 
units of work (thats what is called "live" scope on this page) are easy to 
solve once you think about interception.
 
Not recommended 08 Mar 2005, 11:29 damo9f
If I read this correctly, all code that uses persistence or calls other 
code that might use persistence has to explicitly pass around the 
session.  This leads to tight coupling:
1) moving to another persistence strategy where the dbManager/session is 
not passed around won't work with the code
2) everything needs to know if the objects it works with might use 
persistence, where that's really not the business of the caller

e.g.

void person.updateCart(cart, Session); // bad coupling

this assumes that the "cart" is persistently tracked, rather than 
cached, put into JNDI, updated via a web service, stored in a 
HTTPSession, etc.
 
© Copyright 2006, Red Hat Middleware, LLC. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc. [Privacy Policy]