Logical Design of the Persistence Tier
Logical design refers to the organization, responsibilities, and collaborations of your classes within your application. Before you introduce Hibernate into the mix you need to consider the organization of your persistence-related classes and their interactions with higher tiers. If you don't, you'll only proliferate Hibernate specifics throughout your code in an uncontrolled fashion.
A classic distributed architecture model has distinct tiers or layers, each with well-defined responsibilities and collaborations. Typically the model contains layers devoted to presentation, business, and persistence. Classes are grouped logically into each layer. Two key elements in this design (apart from adopting Spring) can expedite your Hibernate integration:
- Use a rich, common domain model for representing the key domain elements in your system.
- Centralize and encapsulate your persistence logic behind a set of persistence interfaces. Define your persistence services in terms of your domain model.
Common Domain Model
If you don't have one already, a common portable domain model is invaluable for modeling the key concepts in your system and for interchanging data between tiers. Moreover, if you don't have one then mapping onto Hibernate is going to be difficult, as Hibernate is object centric. Think of the domain model as the currency of your application. All application tiers can depend on and use the domain model, including the presentation tier. As surprising as it may seem, many systems do not have a common object model or they have an object model that is non-portable.
Objects that are tied to a particular tier or functional slice must be mapped into intermediate structures in order to get persisted. This approach can lead to a replication, parallel class hierarchies, and unnecessary mapping logic. Hibernate supports rich object-relational mapping semantics so you can safely pass your domain objects down through your higher tiers to Hibernate and let it worry about the rest.
The Data Transfer Object (DTO) pattern is commonly used to aggregate and serialize data from the server to the client tier in a convenient form for the client tier to digest. It is often used in lieu of a common object model. DTOs have arisen in part because EJBs can't be serialized to a remote tier, but also for performance reasons. Consider whether your DTO is worthwhile: does it simply mirror elements of your domain model? Without an entity to worry about, there may be no reason to hang on to your DTO. You can replace it with the real domain model class and be assured that you can pass it to Hibernate without having to write any additional mapping code. Having fewer objects that represent the same logical thing means less code to maintain.
Persistence Service Layer
In a typical J2EE environment, business-/persistence-tier collaborations are managed by the following:
- Wiring up entity beans or JDBC calls directly into the business tier/session beans
- Encapsulating data access operations inside a Data Access Object (DAO)
- A homegrown persistence layer, which wraps or delegates to entities, DAOs, or JDBC
The first approach is fairly messy because persistence operations are interleaved with business operationsthe division of responsibility between the tiers is blurred. Entity/JDBC infrastructure is permeated throughout your code and introducing Hibernate will have a big impact on your higher tiers. Options 2 and 3 offer better models for Hibernate because the low-level data access operations are encapsulated. With a well-defined persistence layer in place, you can hide the implementation details and add Hibernate transparently from the rest of the application.
If you're using a variation on option 1, consider updating your logical design by providing a well-defined persistence layer or DAO set as a precursor to your Hibernate migration. The purpose of this layer is to clearly decouple the persistence from the business logic and to minimize the impact of change. Although this seems like a big job, remember that you'll have to change all your wiring anyway to use Hibernate. You may as well do it in such a way that your persistence tier is clearly defined, centralized, and encapsulated. That way, any subsequent changes are isolated.
In terms of encapsulation, consider whether you want Hibernate exceptions, queries, and sessions to be propagated outside your persistence tier. You may want to wrap the Hibernate session and transform exceptions into more generalized persistence exceptions or exceptions meaningful to the business tier. (In Hibernate 2, the exceptions are checked. In Hibernate 3, the exceptions are unchecked.) Long term, this helps makes your persistence tier agnostic to the underlying implementation. But the corresponding increase in complexity may not be worth itespecially if you don't intend to use anything but Hibernate.
Routing Transparently from Old to New
Modeling your persistence layer in very general terms (i.e., using the base interfaces of your common domain model rather than EJB) helps to abstract your underlying implementation details. You can use interfaces to define the contract presented to and used by the rest of your application. To migrate your existing business code, replace the old entity/JDBC wiring code to have it call your persistence interfaces instead. This pattern is analogous to the way Sun uses the Provider pattern for some parts of the Java API. With JDBC, for example, Sun controls the API contract via a set of interfaces and vendors provide different implementations of the API. Your persistence service is no different. It can provide for pluggable implementations and route persistence requests appropriately. This pattern enables you to route persistence requests to your old entity/JDBC code initially and then delegate it to Hibernate later. Whether you use one or the other is transparent to the business tier.