A Simple Data Access Layer using Hibernate

A Simple Data Access Layer using Hibernate

by
Mario Aquino, Software Engineer
Object Computing, Inc. (OCI)

Introduction

There are a variety of open source tools available today for constructing a data access API, which simplify what has been in the past a complicated and error prone mechanism. Before these tools became available, applications resorted to calling JDBC APIs and passing SQL strings to Statement objects to execute data lookup queries. The lookup calls returned ResultSets that an application would use by calling accessor methods matching the data types of the returned columns. While effective, this approach is fragile because it relies on Strings in application code matching the names of database tables and columns; changing the names of database tables or columns required finding all of their references in the code and changing them. This is problematic if not inelegant.

A better way to interact with a datastore is to eliminate explicit hand-written references to datastore entities while also providing a simple and intuitive API to retrieve and update data that is backed by a database. Preferably, the alternative should not rely on JDBC Statements, SQL strings, or ResultSets. Instead, a solution could create a natural mapping between Java objects and database entities, one that would require a minimum of hand coding to maintain that mapping without removing any of the data retrieval and control facilities provided in the JDBC APIs. This article will introduce two tools that radically simplify the data access development process as well as a lightweight framework built on top of these tools to hide their implementation details from a client application that wishes to leverage the benefits these tools provide.

Hibernate

Hibernate is an open-source object/relational mapping toolkit that relieves the need to make direct use of the JDBC API. Hibernate offers facilities for data retrieval and update, transaction management, database connection pooling, programmatic as well as declarative queries, and declarative entity relationship management. Hibernate also has the ability to generate Java source files to match the structure of a database, as will be discussed in more detail in the next section.

XML files containing configuration data provide Hibernate with details about databases with which it needs to interact. These files contain database connection specifics, connection pooling details, transaction factory settings, as well as references to other XML files that describe tables in the database. Combined, these files provide substantial configurability allowing an application to tune the behaviors and performance of its data access layer to a remarkably fine level of granularity.

Code Generators

One of the keys to a flexible architecture is the ability to leverage code generation tools for as much of the data access layer as possible. Hibernate comes with a code generation component that produces Java source files based on object-relational mapping expressed in its configuration XML files. These files map database table columns to Java class fields, matching equivalent datatypes, identifying primary key fields, and specifying relationships (one-to-one, one-to-many, many-to-one, etc.) between entities. Below is an example of a configuration XML file for an "Order" entity:

<hibernate-mapping>
    <class name="com.ociweb.Order" table="order">
        <id name="id" type="long" column="id">
             <generator class="increment" />
        </id>
        <property name="paymentconfirmed" type="boolean" column="paymentconfirmed" not-null="true"
            length="1"/>
        <property name="installments" type="short" column="installments" not-null="true" length="2"/>
    
        <!-- associations -->
        <!-- many-to-one association to Customer -->
        <many-to-one name="customer" class="com.ociweb.Customer" not-null="true">
            <column name="customerid" />
        </many-to-one>   
        <!-- bi-directional one-to-many association to Orderitem -->
        <set name="orderitems" lazy="true" inverse="true" cascade="all-delete-orphan">
            <key>
                <column name="itemid" />
            </key>
            <one-to-many class="com.ociweb.Orderitem"/>
        </set>
        <!-- one-to-one association to Deliverysite -->
        <one-to-one 
            name="delivery" 
            class="com.ociweb.Delivery" 
            outer-join="auto" 
            property-ref="order"/>    
    </class>
</hibernate-mapping>

This sample XML file tells Hibernate to use a class called "com.ociweb.Order", which should have three primitive fields: a long field called "id", a boolean field called "paymentconfirmed", and a short field called "installments". Additionally, the class will have three fields that represent relationships between the Order and three other entities: the "customer" field (that relates to a Customer object with which it has a many-to-one relationship), an "orderitems" field (that relates to a set of Orderitem objects, here a one-to-many relationship), and a "delivery" field (which points to a Delivery object representing a one-to-one relationship). Hibernate does all the work to maintain relationships between tables in your database, using details specified in the configuration XML files. The XML above also has instructions for Hibernate to load relationship references between tables lazily (the lazy attribute), to treat one of the relationships as bi-directional (the inverse attribute), and to do cascading deletes on child records when their parent records are removed (the cascade attribute). As well, Hibernate can make use of outer join fetching (allowing retrieval of objects with many-to-one or one-to-one relationships as one object) to reduce the number of roundtrips to and from the database. It is configurability features such as these that make Hibernate very powerful.

Another open-source tool called Middlegen already has the ability to connect to a database server and examine the database metadata to discover table definitions and relationships. As well, Middlegen comes with a plugin for Hibernate that allows it to generate the Hibernate configuration files automatically. The Middlegen and Hibernate code generation utilities can be run from Ant targets they each supply.

Utilizing code generating tools for a large portion of the data access layer means that the structure and organization of an application's data model can evolve and that the classes that mirror that structure can be regenerated consistently and immediately. Unfortunately, a changing data model does mean that portions of an application that use the data model's API must also be updated to reflect the new changes. This can be managed relatively painlessly either with modern refactoring tools (which are available in several popular IDEs) or through the use of tools to autogenerate other application areas like the View Layer, which would likely be impacted by changes in the Model.

Now that the code generation and data management tools have been introduced, the rest of the article will focus on the construction of a light-weight data access framework that will separate data clients from the tools that provide the persistence layer.

A Service Layer

It is a good idea to construct a layer to separate the rest of an application from Hibernate so that unnecessary dependencies are not created by our data access toolkit. This layer should be responsible for interfacing with the Hibernate data retrieval APIs and managing transactional boundaries. We should be able to identify an interface pattern to manage access to our domain objects: all will need CRUD (Create, Read, Update, and Delete) methods, a method to find a domain object by its primary key, and a method to find all instances of a domain object. This pattern should hold true for most if not all domain objects that map to entities in the application database. The interface definition below follows this pattern:

public interface DomainObjectMgr {
    public void add(DomainObject obj) throws PeristenceException;
    public void update(DomainObject obj) throws PersistenceException;
    public void delete(DomainObject obj) throws PersistenceException;
    public DomainObject findByPrimaryKey(long id) throws LookupException;
    public Collection findAll() throws LookupException;
}

This interface is used by clients to lookup instances of any "DomainObject" (i.e. an interface exists for each domain class). With similar interfaces declared to manage all of the domain objects of an application, the next component we need is a Service Locator to return references to the implementation of these interfaces. To simplify things, we can define the Service Locator interface with a single method that accepts a Class reference for the domain object manager interface that the client needs. The interface definition below demonstrates this:

public interface ServiceLocator {
    public Object getDomainObjectManager(Class objectManagerInterface) throws ServiceLocatorException;
}

The ServiceLocator could be accessible through a Singleton so that clients could make a simple call to get the domain object manager interface they were interested in:

OrderManager mgr = (OrderManager)GlobalServiceLocator.getInstance().
                        getDomainObjectManager(OrderManager.class);
Order order = mgr.findByPrimaryKey(1000L);
order.setCustomer(someCustomerRef);
mgr.update(order);

The code above is a simple and straight-forward example of the use of this service layer. The client code does not need to know about the implementations of the ServiceLocator or the domain object manager (OrderManager above). Frameworks that make extensive use of interfaces tend to promote ease of testing and the ability to do parallel development. As long as the contracts that an interface provides are well understood, the implementations of interfaces can be mocked-out by their clients until those implementations are completed. Thus the client and the interface implementation are decoupled and can be developed independently. A previous Java News Brief titled "Designing Testability with Mock Objects" focuses on this topic (see the Resources section below for a link).

Each of the operations of the domain object manager's interface can be broken down into independent commands that could be applied generically to all domain objects. Hence an "AddCommand" would be defined to add new domain object records to the data store, the same would be true for "UpdateCommand" and "DeleteCommand". The finder methods can also be made generic. Each appropriate command would be invoked by the implementation class of the domain manager interface. However, rather than create a separate class to implement each domain manager interface, a dynamic proxy can be used to wrap the domain manager interface and invoke the appropriate commands since they all follow the same pattern. The diagram below shows the layout for the interfaces and implementation classes that make up the core of this simple framework.

Service Layer
Figure 1

The Command interface below takes a reference to the method of the domain object manager interface that is being invoked, along with any parameters passed in the invocation, and a reference to a Hibernate Session object. The Hibernate Session interface conveniently provides all of the persistence and query methods that the commands need.

public interface Command {
    Object execute(java.lang.reflect.Method method, Object[] args, net.sf.hibernate.Session session) 
        throws Exception;
}

The ServiceLocatorImpl is mainly responsible for returning a reference to a domain manager given its interface class. All of the interfaces will be implemented by a dynamic proxy that will delegate method invocations to a corresponding command object. So it seems there are three responsibilities that need to be accommodated: providing a means to access each supported command object, validating that the Class objects passed into the getManager() method have methods that follow the domain object manager interface pattern, and creating proxies that will respond to method invocations by delegating to the appropriate commands. The code below demonstrates this.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;

import net.sf.hibernate.Session;
import net.sf.hibernate.HibernateException;
import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;

class ServiceLocatorImpl implements ServiceLocator {
    private static final Map COMMANDS = new Hashtable();
    private static final Command FIND_WITH_NAMED_QUERY_COMMAND = 
                                                new FindWithNamedQueryCommand();
    private List validatedClasses = new Vector();

    public ServiceLocatorImpl() {
        COMMANDS.put("add", new AddCommand());
        COMMANDS.put("update", new UpdateCommand());
        COMMANDS.put("remove", new RemoveCommand());
        COMMANDS.put("findByPrimaryKey", new FindByPrimaryKeyCommand());
        COMMANDS.put("findAll", new FindAllCommand());
    }

    public Object getDomainObjectManager(Class managerClass) 
        throws ServiceLocatorException {
        validate(managerClass);
        return Proxy.newProxyInstance(managerClass.getClassLoader(), 
                                      new Class[]{managerClass},
                                      new ManagerDelegate());
    }

    private void validate(Class managerClass) throws ServiceLocatorException {
        if (!validatedClasses.contains(managerClass)) {
            validateIsInterface(managerClass);
            validateHasDomainObjectMgrAPI(managerClass);
            //cache Class objects that have passed the validity check
            validatedClasses.add(managerClass);
        }
    }

    private void validateIsInterface(Class managerClass) 
        throws ServiceLocatorException {
        if (!managerClass.isInterface()) {
            throw exceptionFactory(managerClass, " is not an Interface");
        }
    }

    private void validateHasDomainObjectMgrAPI(Class managerClass) 
        throws ServiceLocatorException {
        Method[] methods = managerClass.getMethods();
        List mgrMethods = new ArrayList(methods.length);
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            mgrMethods.add(method.getName());
        }
        if (!mgrMethods.containsAll(COMMANDS.keySet())) {
            throw exceptionFactory(managerClass,
                    " must contain all of the following methods: 'add'," +
                    "'update', 'remove', 'findByPrimaryKey', 'findAll'");
        }
    }

    private ServiceLocatorException exceptionFactory(Class managerClass, 
                                                     String message) {
        return new ServiceLocatorException(
            "The supplied Class object (" + managerClass.getName() + ") " +
            message);
    }

    private static class ManagerDelegate implements InvocationHandler {
        private Logger log = LogManager.getLogger(getClass());

        public Object invoke(Object proxy, Method method, Object[] args) 
            throws Throwable {
            Command command = resolveCommand(method);
            if (command == null) {
                throw new UnsupportedOperationException();
            }
            try {
                return command.execute(method, args, getSession());
            } catch (Exception e) {
                invalidateSession();
                throw e;
            }
        }

        private Command resolveCommand(Method method) {
            Command result = (Command) COMMANDS.get(method.getName());
            if (result == null && method.getName().startsWith("find")) {
                //If it is not one of the default commands but it begins 
                //with 'find', assume it is a finder for named queries
                result = FIND_WITH_NAMED_QUERY_COMMAND;
            }
            return result;
        }

        private Session getSession() throws SessionException {
            Session session = ThreadSessionHolder.get();

            if (!session.isConnected()) {
                try {
                    session.reconnect();
                } catch (HibernateException he) {
                    throw new SessionException(
                        "Could not reconnect the session", he);
                }
            }
            return session;
        }

        private void invalidateSession() {
            try {
                ThreadSessionHolder.get().close();
            } catch (HibernateException e) {
                log.error("Unable to close the session");
            }
            ThreadSessionHolder.set(null);
        }
    }
}

The "validity" checking methods above (validateIsInterface() and validateHasDomainObjectMgrAPI()) only really test that the passed in Class object is an interface that contains all of the methods described in the domain object manager pattern. The signatures of those methods are not also checked, which could be an area for improvement. For simplicity's sake in this article, the name check is good enough.

Another noteworthy aspect of the implementation above appears in the getSession() method of the ManagerDelegate inner class, where a call is made to a ThreadSessionHolder to retrieve a reference to a Hibernate Session object. As its name implies, the ThreadSessionHolder associates a reference to a Hibernate Session to the currently executing thread. While the Hibernate Session is used to retrieve and update objects that are mapped to database table rows, it also acts as a cache for those persistent objects keeping references to objects it has retrieved. By associating the Session with the current executing thread, objects retrieved through different domain object managers will be able to establish and remove relationships between themselves without having to explicitly share the same Session; since the Session is tied to the thread that all domain object managers are running on, Session sharing happens transparently. A consequence of this design, however, is that the scope of the Session needs to be managed somehow. In a J2EE application, Session scoping can happen at the request level so that each domain object manager invoked during a single request shares a common Session that is closed at the return of a response. For a non-J2EE application, a similar model is possible though it requires a component with the responsibility of defining a "request".

FindWithNamedQuery

The resolveCommand() method in the ManagerDelegate above tries to retrieve a Command object whose name matches that of the invoked Method. If it doesn't find one, it tests whether the name of the invoked Method begins with 'find', and if so returns a command that hasn't yet been discussed, the FindWithNamedQuery command. This command utilizes Hibernate's named query capability, whose mechanics will be discussed in a later section. The command adds support for arbitrary finder queries which may appear in the domain object manager interface. The only requirements for these queries is that their methods begin with "find" and return a java.util.Collection reference and that a named query matching the method name appear in the XML configuration file of the domain object whose manager interface contains the finder method. For example, a finder method could be added to the DomainObjectMgr interface above that found instances of DomainObject by name:

public Collection findDomainObjectByName(String name) throws LookupException;

The Hibernate configuration XML file for DomainObject would need a <query/> element with the name and details of the query in HQL:

<query name="com.ociweb.domain.DomainObjectMgr.findDomainObjectByName"><![CDATA[
    from com.ociweb.bean.DomainObject as domainObject
    where domainObject.name = ?]]>
</query>

The query defined in the mapping file must be named according to the fully qualified class name of the domain manager interface plus the name of the finder method itself (" com.ociweb.domain.DomainObjectMgr.findDomainObjectByName "). Additionally, this query takes a parameter that the finder method in the DomainObjectMgr interface defines as a String. The order and types of parameters declared in the finder method signature must follow the parameters expected by the query definition.

Transactions

As Figure 1 above shows, three of the commands extend from a TransactionalCommand base class. The purpose of this class is to wrap the execution of a command within a transaction. This class follows the Gang of Four Template Method pattern, which starts a transaction at the invocation of its execute() method then calls an abstract method (command()) which must be implemented by its child classes. The TransactionalCommand class appears below.

import net.sf.hibernate.Session;
import net.sf.hibernate.Transaction;
import net.sf.hibernate.HibernateException;

abstract class TransactionalCommand implements Command {

    public Object execute(java.lang.reflect.Method method, Object[] args, Session session) 
        throws Exception {
        if (args == null || args[0] == null) {
            throw new PersistenceException("Null target record cannot be added, updated, or removed");
        }
        Transaction txn = session.beginTransaction();
        try {
            Object result = command(args, session);
            txn.commit();
            return result;
        } finally {
            if (!txn.wasCommitted()) {
                txn.rollback();
            }
        }
    }

    protected abstract Object command(Object[] args, Session session) 
        throws Exception;
} //end TransactionalCommand

The AddCommand shown below is small and simple, which is a testament to the ease of use of the Hibernate APIs.