Download A Simple Data Access Layer using Hibernate

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts

Extensible Storage Engine wikipedia , lookup

Microsoft Jet Database Engine wikipedia , lookup

Entity–attribute–value model wikipedia , lookup

Open Database Connectivity wikipedia , lookup

Relational model wikipedia , lookup

Clusterpoint wikipedia , lookup

Database model wikipedia , lookup

Versant Object Database wikipedia , lookup

Transcript
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-toone 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.
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
import
import
import
java.lang.reflect.InvocationHandler;
java.lang.reflect.Method;
java.lang.reflect.Proxy;
java.util.*;
import
import
import
import
net.sf.hibernate.Session;
net.sf.hibernate.HibernateException;
org.apache.log4j.Logger;
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

Zip file containing all source code for the sample framework (3.7M size)







Zip file containing HTML representation of the sample framework source code (160K size)
Hibernate - http://www.hibernate.org
Middlegen Hibernate Plugin - http://prdownloads.sourceforge.net/hibernate/Middlegen-Hibernate-r3.zip
Middlegen - http://boss.bekk.no/boss/middlegen/
Ant - http://ant.apache.org/
JDBC - http://java.sun.com/products/jdbc/

Designing Testability with Mock Objects - http://www.ociweb.com/jnb/jnbJun2003.html
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 and
Open Enrollment classes.
Course Catalog | Open Enrollment Schedule
For further information regarding OCI's Educational Services programs, please visit our Educational Services section
on the web or contact us at [email protected].
Object Computing, Inc. is a Sun Authorized Java Center in St. Louis, MO and a Member of the Object Management
Group, OMG. OCI specializes in distributed computing using object-oriented and web-enabled technologies and
provides Consulting, Education and Product Development services to clients nation-wide. For more information contact
us in St. Louis, MO (314)579-0066, Tempe, AZ (480)752-0042 or email [email protected].
Inquiries regarding Career Opportunities can be directed to: [email protected].
The Java News Brief is a monthly newsletter. The purpose and intent of this publication is to advance Java, provide
technical value and announce available OCI educational services. To subscribe or unsubscribe from this newsletter,
simply click the desired link.