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.

import net.sf.hibernate.HibernateException;

class AddCommand extends TransactionalCommand {
    protected Object command(Object[] args, net.sf.hibernate.Session session) 
        throws Exception {
        try {
            return session.save(args[0]);
        } catch (HibernateException e) {
            throw new PersistenceException(
                "Unable to add new object to the datastore", e);
        }
    }
} //end AddCommand

The other two transactional commands (UpdateCommand and RemoveCommand) follow the exact same simple style of the AddCommand.

Flexible Querying

Along with a simple API for retrieving and persisting objects mapped to relational tables, Hibernate also provides a very robust querying API that supports query strings, named queries, and queries built as aggregate expressions. The querying API is exposed through two interfaces, the Query interface and the Criteria, with the former supporting queries passed in as Strings in "Hibernate Query Language" (HQL) syntax (or through the invocation of predefined "named" queries, which will be examined later in this section) and the latter designed for aggregate queries.

Query

The Hibernate Session interface acts as a factory for Query objects, which can be created either by passing a String in HQL syntax (which is similar to SQL, though object-oriented and able to understand inheritance and polymorphism) to the createQuery() method or by calling the getNamedQuery() method and passing it the name of the query that has been defined in one of the configuration XML files for Hibernate. Hibernate Query objects can perform lookups using static values (e.g. "from com.ociweb.Customer as customer where customer.name='Als Petstore'") as well as parameterized queries (e.g. "from com.ociweb.Customer as customer where customer.name=?"). In fact, parameterized queries can be stated in two forms, one where the positional question marks ("?") can be replaced with parameter values assigned according to the ordering of the question marks in the query string, or another where named parameters are used instead of the positional questions marks. A "named parameter" query string might look like this: "from com.ociweb.Customer as customer where customer.name=:name". The :name parameter would be replaced in a call where the parameter name and the associated value are both passed in (Query.setString(String name, String value)). Advantages of using named parameters are that the ordering of the components in the where clause can change without affecting the query and that parameters can appear multiple times in a query where the values should be the same.

Named queries also relieve an application of hard-coded column names and join specifics, which makes an application resistant to change. As well, relying on named queries defined outside of the application's code means that the application does not have to "know" Hibernate Query Language syntax, further insulating it from the persistence mechanism which can more easily be changed without affecting the client code.

Criteria

The Criteria interface approaches querying by allowing a client to build an aggregation of query clauses. Just as with the Query interface, the Hibernate Session acts as a factory for Criteria, taking a Class reference of the entity class for which a Criteria will be defined and returning an "empty" Criteria. The aggregation happens through the use of the Expression class, which has methods for building discrete comparative expressions (like "greater than", "less than", "like", "between", "equals", "and", "not", etc.) that can be found in regular SQL query strings. Static factory methods on the Expression class return these Expression components, which can be aggregated by the Criteria to form a composite query.

Wrapping the Query APIs

The simple data access layer described in the first half of this article acts to separate the client application from the underlying persistence mechanism. Making the rich query capabilities available to a client application while at the same time avoiding an explicit dependency on Hibernate means that the query APIs need to be wrapped by a delegation layer. This layer need not provide any functionality in itself, other than exposing a simple way to create queries and shielding the client from Session or other Hibernate specific details. Luckily, Hibernate supports externalized queries (via the named query capability), so client applications can make use of this through the wrapping layer to leverage powerful query facilities while leaving Hibernate's query language out of the picture.

Putting It All Together

A review of the components described in this article shows that with an existing database definition, tools can generate both Hibernate XML configuration files that provide a mapping between database entities and Java objects as well as the source code for the Java object classes themselves. Furthermore, a lightweight data access layer can provide a simple and generic means of leveraging the strengths and flexibilities of the Hibernate persistence toolkit without imposing a compile time dependency on Hibernate for the client application. So what has this bought us? A totally reusable persistence framework that makes use of code generation so that an application can evolve over time, introducing new entities and relationships (or adjusting existing ones) with a minimum of hand-coding necessary for support. The framework is also generic enough to be extended so that Hibernate need not be the underlying data storage manager (or perhaps not the only data storage manager). Other O/R mapping or even Java Data Objects (JDO) toolkits could be integrated into the framework in a transparent way to provide services that Hibernate on its own may not.

Conclusion

There are an abundance of tools and technologies that provide solutions to problems that all data-driven applications share, namely accessing and managing a data model. Two of those tools, Hibernate and Middlegen, combine to automatically generate an object representation of an entity model. This article has demonstrated a simple data access framework that leverages the strengths of Hibernate while providing a layer of separation so that an application need not be dependent on its data access toolkit. The combination of code generation and toolkit abstraction boosts an application's ability to quickly evolve and adjust as need warrants and more robust solutions become available.

Resources


OCI Educational Services

OCI is the leading provider of Object Oriented technology training in the Midwest. More than 3,000 students participated in our training program over the last 12 months. Targeted toward Software Engineers and the development community, our extensive program of over 50 hands-on workshops is delivered to corporations and individuals throughout the U.S. and internationally. OCI's Educational Services include Group Training events, Open Enrollment classes, and Courseware Licensing.

JavaC/C++.NET/C#Real-Time SystemsObject Oriented Software EngineeringDistributed ComputingWireless EnterpriseUnix/LinuxXML

For further information regarding OCI's Educational Services programs, please visit our Educational Services section on the web or contact us at training@ociweb.com.


posted on 2004-12-31 21:28  笨笨  阅读(1620)  评论(0编辑  收藏  举报

导航