Home

Advertisement

Hibernate Transactions and Units of Work

I won't go into depth about Hibernate or the design considerations that it brings, as there is ample documentation on the Hibernate web site. One particularly useful document is Session and Transaction Scope, which elucidates on the concepts of application (user) transactions and units of work. Suffice to say, that having experimented amply with both design patterns, session-per-request and session-per-application-transaction, I strongly prefer the former, as it provides a lot more insight and control over when data is persisted. I find it is much easier and there are far fewer surprises than with long application transactions.

I would like to note that in most cases there is a very fine line between the session-per-request pattern and what the author of the above document calls a session-per-operation anti-pattern. It is common to load an object into memory in one request and save it in the next request. Two operations in two requests averages out to be one session per operation. Because sessions are lightweight and because they cache data I do not consider session-per-operation to be anti-pattern from any standpoint other than performance, which can be remedied using a second-level cache. Of course, whenever possible it is always best to group operations in a single unit of work to enhance performance, provided it is convenient and you are not violating any transaction semantics by doing so.

The most common method for implementing the session-per-request pattern is to use the ThreadLocal Session pattern, which works well in a servlet container using filters, or in a Struts action class. It does not, however, isolate all of the concerns that are typical of every transaction. For example, although the commitTransaction method of HibernateUtil will attempt a rollback if the transaction commit fails, it will not attempt a rollback if there is an error prior to the commit, such as when a business exception that occurs mid-transaction.

The ideal solution would be to isolate those aspects (transaction error handling and recovery) by wrapping the persistence operations with a boilerplate try/catch/finally block. This could be done by generating proxies for servlets, JSF backing beans, or Struts actions, but there is no support for that at this time. The Spring framework AOP facilities may be able to accomodate this approach, but I am not familiar with Spring.

The solution I came up with was to create an abstract class called UnitOfWork with an abstract method named doWork( ) and a final method named execute( ) that wraps doWork( ) with the appropriate error handling logic. In the request handler methods, anonymous inner classes extending UnitOfWork can be created inline that contain the persistence operation. Here is the code:

UnitOfWork.java
public abstract class UnitOfWork
{
    private Throwable _transactionException = null;
    private Throwable _commitException = null;
    private Throwable _rollbackException = null;
    private Throwable _closeException = null;
    
    private boolean _committed = false;
    private boolean _rolledBack = false;
    private boolean _closed = false;
    
    protected Session _session = null;
    protected Transaction _transaction = null;
    
    public Throwable getTransactionException( )
    {
        return _transactionException;
    }    
    
    public Throwable getCommitException( )
    {
        return _commitException;
    }
    
    public Throwable getRollbackException( )
    {
        return _rollbackException;
    }
    
    public Throwable getCloseException( )
    {
        return _closeException;
    }
    
    public boolean isCommitted( )
    {
        return _committed;
    }
    
    public boolean isRolledBack( )
    {
        return _rolledBack;
    }
    
    public boolean isClosed( )
    {
        return _closed;
    }
    
    public abstract Object doWork( Object[] arguments )
    throws Throwable;
    
    private final void init( )
    throws Throwable
    {
        _transactionException = null;
        _commitException = null;
        _rollbackException = null;
        _closeException = null;
    
        _committed = false;
        _rolledBack = false;
        _closed = false;
    }
    
    public final Object execute( )
    throws UnitOfWorkException
    {
        return execute( null );
    }
    
    public final Object execute( Object[] arguments )
    throws UnitOfWorkException
    {
        Object returnValue = null;
        try
        {
            _session = HibernateUtil.openSession( );
            _transaction = _session.beginTransaction( );
            init( );
            returnValue = doWork( arguments );
            try
            {
                commit( );
                _committed = true;
            }
            catch( Throwable commitException )
            {
                _commitException = commitException;
            }    
        }
        catch( Throwable transactionException )
        {
            _transactionException = transactionException;
        }
            
        if( !_committed )
        {
            try
            {
                rollback( );
                _rolledBack = true;
            }
            catch( Throwable rollbackException )
            {
                _rollbackException = rollbackException;
            }
        }

        try
        {
            close( );
            _closed = true;
        }
        catch( Throwable closeException )
        {
            _closeException = closeException;
        }
        
        _session = null;
        _transaction = null;
        
        if( !_committed )
        {
            throwException( );
        }
        else if( !_closed )
        {
            logException( );
        }
        return returnValue;
    }
    
    public void commit( )
    throws Throwable
    {
        if( _transaction != null )
        {
            _transaction.commit( );
        }
    }
    
    public void rollback( )
    throws Throwable
    {
        if( _transaction != null )
        {
            _transaction.rollback( );
        }
    }
    
    public void close( )
    throws Throwable
    {
        if( _session != null )
        {
            _session.close( );
        }
    }
    
    private void throwException( )
    throws UnitOfWorkException
    {
        UnitOfWorkException uowe = buildException( );
        if( uowe != null )
        {
            throw uowe;
        }
    }
    
    private UnitOfWorkException buildException( )
    {
        UnitOfWorkException uowe = null;
        if( _transactionException != null )
        {
            uowe = new UnitOfWorkException( this, _transactionException );
        }
        
        if( (_commitException != null) && (uowe == null) )
        {
            uowe = new UnitOfWorkException( this, _commitException );
        }
        
        if( (_rollbackException != null) && (uowe == null) )
        {
            uowe = new UnitOfWorkException( this, _rollbackException );
        }
        
        if( (_closeException != null) && (uowe == null) )
        {
            uowe = new UnitOfWorkException( this, _closeException );
        }
        
        return uowe;
    }
    
    private void logException( )
    {
        UnitOfWorkException uowe = buildException( );
        // Commons logging, J2SE logging, Log4j, or System.out.println here
    }
}


UnitOfWorkException.java
public class UnitOfWorkException
extends RuntimeException
{
    private UnitOfWork _unitOfWork = null;
    
    public void setUnitOfWork( UnitOfWork unitOfWork )
    {
        _unitOfWork = unitOfWork;
    }
    public UnitOfWork getUnitOfWork( )
    {
        return _unitOfWork;
    }
    
    public UnitOfWorkException( )
    {
    }
    
    public UnitOfWorkException( String message )
    {
        super( message );
    }
    
    public UnitOfWorkException( Throwable cause )
    {
        super( cause );
    }
    
    public UnitOfWorkException( String message, Throwable cause )
    {
        super( message, cause );
    }
    
    public UnitOfWorkException( UnitOfWork unitOfWork )
    {
        _unitOfWork = unitOfWork;
    }
    
    public UnitOfWorkException( UnitOfWork unitOfWork, String message )
    {
        super( message );
        _unitOfWork = unitOfWork;
    }
    
    public UnitOfWorkException( UnitOfWork unitOfWork, Throwable cause )
    {
        super( cause );
        _unitOfWork = unitOfWork;
    }
    
    public UnitOfWorkException( UnitOfWork unitOfWork, String message, Throwable cause )
    {
        super( message, cause );
        _unitOfWork = unitOfWork;
    }
    
    public void printStackTrace( PrintStream out )
    {
        printStackTrace( new PrintWriter( out ) );
    }
    
    public void printStackTrace( PrintWriter out )
    {
        super.printStackTrace( out );
        if( _unitOfWork != null )
        {
            if( _unitOfWork.getCommitException( ) != null )
            {
                out.print( "Upon commit: " );
                _unitOfWork.getCommitException( ).printStackTrace( out );
            }

            if( _unitOfWork.getRollbackException( ) != null )
            {
                out.print( "Upon rollback: " );
                _unitOfWork.getRollbackException( ).printStackTrace( out );
            }

            if( _unitOfWork.getCloseException( ) != null )
            {
                out.print( "Upon close: " );
                _unitOfWork.getCloseException( ).printStackTrace( out );
            }
        }
    }
}


And here is an example of how it would be used as an anonymous inner class:

Example 1
...
    final String itemName = "Example";
    Item item = (Item)new UnitOfWork( )
    {
        public Object doWork( Object[] arguments )
        throws Throwable
        {
            String name = (String)arguments[0];
            // Create a new ItemDao using the session variable prepared
            // by the UnitOfWork execute( ) method and placed in the
            // _session instance variable of the UnitOfWork class
            return new ItemDao( _session ).getByName( name );
        }
    }.execute( new Object[] { itemName } );
    System.out.println( "My item: " + item );
    ...


In this example, all of the error handling and recovery has been replaced with an anonymous inner class declaration. Arguments and return values can be passed to and from the doWork( ) method through the execute( ) method. The only possible exception that can be thrown from execute( ) is a UnitOfWorkException, which has placeholders for exceptions that can occur at all phases of the execute( ) method. When the method returns, the programmer can be assured that all cleanup has been accomplished and the best possible effort was made to gracefully recover from any errors that might have occurred. The init( ), commit( ), rollback( ), and close( ) methods can be overridden to perform additional initialization and cleanup operations as needed (file descriptors, Oracle temporary LOBs, etc.)

Note that outer class instance variables are accessible within inner classes, so we could have changed the code to be like this:

Example 2
public class MyClass
{
    private Item _item = null;
    private final String _itemName = "Example";

    ....

    public void myMethod( )
    {
        ...
        new UnitOfWork( )
        {
            public Object doWork( Object[] arguments )
            throws Throwable
            {
                _item = new ItemDao( _session ).getByName( _itemName );
                return null;
            }
        }.execute( );
        System.out.println( "My item: " + item );
        ...
    }
    ...
}


Some notes about this design pattern:


  • The transaction and error handling logic operates only on the session created by the execute( ) method

    • The code in the doWork( ) method is free to use any available session, or create its own, but doing so circumvents the error handling logic and defeats the purpose of the UnitOfWork class

    • All work should be done using the session object stored in the _session instance variable, either directly or through Data Access Objects (DAOs) that use the session specified by the caller



  • There is no need for a filter or other interceptor

  • The session and transactions are opened and closed in the execute( ) method. All initialization and cleanup is transparent to the programmer.

    • Lazy associations must be fetched within a UnitOfWork doWork( ) method

    • All objects are detached outside of the UnitOfWork doWork( ) method

    • Changes made to detached objects will not persist unless the objects are explicitly updated or merged



  • A UnitOfWork anonymous class be used to implement either the session-per-operation pattern (or anti-pattern if you like) or the session-per-request pattern depending on the content of the doWork( ) method.

    • The contents of the doWork( ) method can consist of one operation or a hundred, so long as they all belong to one logical database transaction.



  • Works well with servlets and Struts

    • Servlet class can extend UnitOfWork and implement javax.servlet.Servlet. The service( ) method of the servlet can perform some initialization and simply call execute( ) when ready (no anonymous inner class required).

    • Struts action classes require anonymous inner classes (or outside helper classes), as Action classes must extend Action, and there may be more than one unit of work if DispatchAction is used.

    • JSF beans should use anonymous inner classes (or outside helper classes) as there will likely be more than one action method per backing bean



Comments

January 2007

S M T W T F S
 123456
78910111213
14151617181920
21222324252627
28293031   
Powered by LiveJournal.com

Advertisement

Customize