ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Object-Relational Mapping with Apache Jakarta OJB

by Charles Chan
01/08/2003

As enterprise applications become more complex, it becomes more challenging to map their object models to relational database tables. In typical J2EE applications, developers use EJB entity beans as the abstraction for the underlying object store. Unfortunately, entity beans, as of EJB 2.0, have limited mapping capabilities. For example, custom data conversions (such as from Currency objects to Strings) and component inheritance (Manager is an Employee) are not directly supported. When the object models require these features, developers have to write and maintain extra code. Even so, the results are often not satisfactory.

Fortunately, there are alternatives. These persistence frameworks allow developers to map normal Java objects to relational tables with minimal effort. The Java standard for such a framework is called JDO (Java Data Objects), and it is maturing. Unfortunately, there is no complete, free implementation of JDO today. This makes evaluating this emerging technology more difficult. However, with some planning and the use of best practices, it is not difficult to develop your application prototype with a non-standard persistence framework, later switching to an industrial-strength JDO implementation if it suits your business.

Three of the most popular persistence frameworks in the open source community are Hibernate, Castor, and OJB. In this article, we will focus on OJB. OJB integrates smoothly into J2EE containers with full support of JTA and JCA, and is a viable alternative to EJB entity beans. Since OJB does not modify your source code or byte code in any way, you can make an object persistent even without access to the original source code. This article introduces OJB and focuses on its component-inheritance capabilities. The Appendix presents some best practices to show you how to decouple your application from the specifics of a persistence layer.

Related Reading

Java Enterprise Best Practices
By The O'Reilly Java Authors

1. Obtaining OJB

OJB can be downloaded from the Apache Jakarta Project home page. We will use version 0.9.7, the latest as of this writing, throughout this article. As a member of the Jakarta family, OJB makes extensive use of the Jakarta Commons Library. This is good news to everyone who is already familiar with the Commons library through other Jakarta projects like Struts.

Installation is a snap. You only need to unzip the downloaded file into any directory and you are ready to go.

2. Component Inheritance

Component inheritance represents an "IS-A" relationship between two objects in the object store. EJB entity beans do not directly support component inheritance. You can mimic the relationship in entity beans by creating a one-to-one relationship, delegating all base class getters and setters to the base class entity bean. Unfortunately, this is only a halfway solution, because EJB finders cannot return the proper object type.

To showcase the component inheritance feature of OJB, we will use an example throughout this section. We will walk you through the entire process of creating and running this example to help you understand the steps involved. The example attempts to persist two objects: Manager and Employee. They are related by a "IS-A" relationship. See Figure 1 below.

Employee and Manager classes

Figure 1: The Employee and Manager classes

2.1 Interfaces

Example 1: Component Inheritance Example Interfaces

public interface Employee
{
    public String getName();
    public void setName(String name);
}

public interface Manager extends Employee
{
    public Integer getProjectNumber();
    public void setProjectNumber(Integer projectNumber);
}

The first step in our example is to create interfaces for our object model. It is always a good idea to have interfaces for your object model because interfaces allow your implementations to evolve without causing breakage in other areas of your system. This is especially true for objects in the persistence layer. For example, a version timestamp is used to support object versioning in the persistence layer. It is important in the persistence layer, but not so in the business layer. Using interfaces allow you to change the object versioning scheme without breaking the business layer.

Using interfaces also allows you to switch the underlying persistence framework with mininal effort.

2.2 Base Class Implementation

After the interfaces are done, we are ready for the implementation. Since different mapping techniques require slightly different implementations (see next page), a base class implementation will be helpful to house the common functionalities.

Example 2: Base Class Implementation

public class BasePersistenceObjectImpl
{
    private Integer id;

    public BasePersistenceObjectImpl()
    {
        super();
    }

    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    public boolean equals(Object o)
    {
        if (o == this)
        {
            return true;
        }
        else if (o != null && o instanceof BasePersistenceObjectImpl)
        {
            return ((BasePersistenceObjectImpl) o).id.equals(this.id);
        }
        else
        {
            return false;
        }
    }
}

2.3 Mapping Inheritance

Here comes the real meat of the example. There are three common techniques to map component inheritance to relational tables. They are:

  1. Map each class onto a distinct table, each with its own base class attributes.

  2. Map all classes in a hierarchy onto one table.

  3. Map each class into a distinct table, each with a foreign key to its base class table, where base class attributes can be found.

Each technique has its own pros and cons. In the following sections, we will discuss each one of them in detail.

2.3.1 One Table Per Class


2.3.1.1 Table Schema

This technique puts all base class attributes into the subclass tables. The base class table and the subclass table are not related in the schema.

Example 3: Table Schema For Mapping Strategy One

CREATE TABLE employee (
  id   INTEGER NOT NULL,
  name VARCHAR(50) NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE manager (
  id             INTEGER NOT NULL,
  name           VARCHAR(50) NOT NULL,
  project_number INTEGER NOT NULL,
  PRIMARY KEY (id)
);

2.3.1.2 Implementation

The implementation is straightforward and requires no special OJB changes:

Example 4: Implementations For Mapping Strategy One


public class EmployeeImpl extends BasePersistenceObjectImpl implements Employee
{
    private String name;

    public void setName(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }
}

public class ManagerImpl extends EmployeeImpl implements Manager
{
    private Integer projectNumber;

    public ManagerImpl()
    {
        super();
    }

    public Integer getProjectNumber()
    {
        return projectNumber;
    }

    public void setProjectNumber(Integer projectNumber)
    {
        this.projectNumber = projectNumber;
    }
}

2.3.1.3 OJB Descriptor

The OJB descriptor describes how a Java class and its attributes map to a table. In OJB, we denote inheritance using the <extent-class> element in the descriptor file. In this case, EmployeeImpl is the base class of ManagerImpl. Once the extent-class is in place, if a query is performed against EmployeeImpl, objects that extend EmployeeImpl (e.g. ManagerImpl) will also be considered for matching.

Example 5: OJB Descriptor For Mapping Strategy One

<class-descriptor class="ojbtest.impl.distinct.EmployeeImpl"
                  table="EMPLOYEE">
  <extent-class class-ref="ojbtest.impl.distinct.ManagerImpl"/>
  <field-descriptor id="1"
                    name="id"
                    column="ID"
                    jdbc-type="INTEGER"
                    primarykey="true"
                    autoincrement="true"/>
  <field-descriptor id="2"
                    name="name"
                    column="NAME"
                    jdbc-type="VARCHAR"/>
</class-descriptor>

<class-descriptor class="ojbtest.impl.distinct.ManagerImpl"
                  table="MANAGER">
  <field-descriptor id="1"
                    name="id"
                    column="ID"
                    jdbc-type="INTEGER"
                    primarykey="true"
                    autoincrement="true"/>
  <field-descriptor id="2"
                    name="name"
                    column="NAME"
                    jdbc-type="VARCHAR"/>
  <field-descriptor id="3"
                    name="projectNumber"
                    column="PROJECT_NUMBER"
                    jdbc-type="INTEGER"/>
</class-descriptor>

2.3.1.4 Pros & Cons

This approach has two main advantages. First, the table schemas are separated cleanly. No joins are necessary to query one type of object. Second, database constraints can be applied.

Despite these advantages, this approach suffers from performance problems. Consider the case when you need to query all Employees. Both the MANAGER table and the EMPLOYEE table will be queried. The performance penalty grows proportionally to the size of the class hierarchy. Also, the duplicated base class columns in each subclass table make adding an attribute to the base class more difficult.

2.3.2 One Table Per Class Hierarchy


2.3.2.1 Table Schema

With this technique, the base class and all of its subclasses are stored in one table. The table schema contains attributes of all classes in the same hierarchy:

Table 6: Table Schemas For Mapping Strategy Two

CREATE TABLE employee (
  id             INTEGER NOT NULL,
  class_name     VARCHAR(255),
  name           VARCHAR(50) NOT NULL,
  project_number INTEGER,
  PRIMARY KEY (id)
);

Notice that a new column, CLASS_NAME, is used by OJB to identify the type of object a table row represents.

2.3.2.2 Implementation

To use this mapping technique, OJB requires a special attribute (ojbConcreteClass) in your base class. This special attribute must be initialized to the proper subclass class name during object initialization.

Example 7: Implementations For Mapping Strategy Two

public class EmployeeImpl extends BasePersistenceObjectImpl implements Employee
{
    /** the special attribute telling OJB the object's concrete type.
     *  NOTE: this attribute MUST be called ojbConcreteClass
     */
    protected String ojbConcreteClass;
    private String name;

    public String getOjbConcreteClass()
    {
        return ojbConcreteClass;
    }

    public void setOjbConcreteClass(String ojbConcreteClass)
    {
        this.ojbConcreteClass = ojbConcreteClass;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }
}

public class ManagerImpl extends EmployeeImpl implements Manager
{
    private Integer projectNumber;

    public ManagerImpl()
    {
        super();
        ojbConcreteClass = ManagerImpl.class.getName();
    }

    public Integer getProjectNumber()
    {
        return projectNumber;
    }

    public void setProjectNumber(Integer projectNumber)
    {
        this.projectNumber = projectNumber;
    }
}

2.3.2.3 OJB Descriptor

In the descriptor, we specify that the ojbConcreteClass attribute is mapped to the CLASS_NAME table column. This allows queries to return objects of the right type:

Table 8: OJB Descriptor For Mapping Strategy Two

<class-descriptor class="ojbtest.impl.hierarchy.EmployeeImpl"
                  table="EMPLOYEE">
  <extent-class class-ref="ojbtest.impl.hierarchy.ManagerImpl"/>
  <field-descriptor id="1"
                    name="id"
                    column="ID"
                    jdbc-type="INTEGER"
                    primarykey="true"
                    autoincrement="true"/>
  <field-descriptor id="2"
                    name="ojbConcreteClass"
                    column="CLASS_NAME"
                    jdbc-type="VARCHAR"/>
  <field-descriptor id="3"
                    name="name"
                    column="NAME"
                    jdbc-type="VARCHAR"/>
</class-descriptor>

<class-descriptor class="ojbtest.impl.hierarchy.ManagerImpl"
                  table="EMPLOYEE">
  <field-descriptor id="1"
                    name="id"
                    column="ID"
                    jdbc-type="INTEGER"
                    primarykey="true"
                    autoincrement="true"/>
  <field-descriptor id="2"
                    name="ojbConcreteClass"
                    column="CLASS_NAME"
                    jdbc-type="VARCHAR"/>
  <field-descriptor id="3"
                    name="name"
                    column="NAME"
                    jdbc-type="VARCHAR"/>
  <field-descriptor id="4"
                    name="projectNumber"
                    column="PROJECT_NUMBER"
                    jdbc-type="INTEGER"/>
</class-descriptor>

2.3.2.4 Pros & Cons

This approach has two main advantages. First, no table joins are necessary to query objects in the same hierarchy. Second, adding a new class to the hierarchy has very little overhead.

This approach has its own problems, though. For example, database constraints must be relaxed to accommodate all attributes in the class hierarchy. Also, it is not easy to identify from the table schema which attributes belong to which class.

2.3.3 One Table Per Class, with a Foreign Key to the Base Class

This technique creates a table for each class in the hierarchy. Unlike the first approach, the base class attributes are not duplicated in the subclass tables. Instead, each subclass table uses a special foreign key to refer back to the base class. OJB does not directly support this model.

This technique has two main advantages. First, the subclass tables do not duplicate any base class columns. Second, database constraints can be applied.

However, this technique suffers from excessive table joins in queries. The performance gets worse as the class hierarchy deepens.

After evaluating your options, you can decide which mapping technique suits you best. The next step in our example is to create the OJB deployment descriptor.

2.4 Deploying and Running

2.4.1 Editing the Deployment Descriptor

repository.xml is the OJB deployment descriptor. Its purpose is very much like that of the ejb-jar.xml descriptor. OJB comes with a sample repository.xml file that you can modify for your application. You should copy the following files to your project directory: repository.xml, repository_internal.xml, repository.dtd, and OJB.properties.

Example 9: A Sample repository.xml File

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE descriptor-repository SYSTEM "repository.dtd" [
<!ENTITY user SYSTEM "repository_user.xml">
<!ENTITY internal SYSTEM "repository_internal.xml">
]>

<descriptor-repository version="0.9.6"
                       isolation-level="read-uncommitted">

   <!-- The Default JDBC Connection. If a class-descriptor does not
        specify its own JDBC Connection, the Connection specified here
        will be used. -->

   <jdbc-connection-descriptor
   		platform="Hsqldb"
   		jdbc-level="2.0"
   		driver="org.hsqldb.jdbcDriver"
   		protocol="jdbc"
   		subprotocol="hsqldb:hsql"
   		dbalias="//localhost"
   		username="sa"
   		password=""/>

    <!-- include user defined mappings here -->
    &user;

    <!-- include ojb internal mappings here -->
    &internal;

</descriptor-repository>

The sample repository.xml file includes two XML fragments (external entities), repository_internal.xml and repository_user.xml. You have already copied repository_internal.xml to your project directory. What about repository_user.xml? It is the OJB descriptor fragment we have covered in the previous sections. You need to create a file called repository_user.xml and put the mapping details inside. All three XML files and fragments must be in your application's classpath.

2.4.2 Setup OJB Internal Tables

Before we can run our applications, we must set up the OJB internal tables. This step is done once and is relatively painless. The steps below should be helpful. (Assume OJB_HOME points to where you've unzipped the OJB distribution.)

  1. Modify OJB_HOME/build.properties. At the beginning of the file, uncomment the appropriate database you are planning to use. If you plan to use Hypersonic database, that part of the properties file should look like this:

    Table 10: OJB build.properties

    profile=hsqldb
    #profile=mssqldb
    #profile=mysql
    #profile=db2
    #profile=oracle
    #profile=msaccess
    #profile=postgresql
    #profile=informix
    #profile=sybase
    #profile=sapdb
  2. In the OJB_HOME/profile directory, modify the corresponding database profile file. For the Hypersonic database, the file is called hsqldb.profile. In general, you only need to make sure that all of the database URLs are correct. In the case of Hypersonic, the file should be:

    Example 11: OJB Database Profile

    dbmsName = Hsqldb
    jdbcLevel = 2.0
    urlProtocol = jdbc
    urlSubprotocol = hsqldb:hsql
    urlDbalias = //localhost
    
    createDatabaseUrl = ${urlProtocol}:${urlSubprotocol}:${urlDbalias}
    buildDatabaseUrl = ${urlProtocol}:${urlSubprotocol}:${urlDbalias}
    databaseUrl = ${urlProtocol}:${urlSubprotocol}:${urlDbalias}
    databaseDriver = org.hsqldb.jdbcDriver
    databaseUser = sa
    databasePassword =
    databaseHost = 127.0.0.1

    (Note: The createDatabaseUrl, buildDatabaseUrl, and databaseUrlvariables are all incorrect in the OJB 0.9.7 distribution. Make sure the URLs are right after string substitutions are done.)

  3. If you have Ant installed, you can simply invoke ant prepare-testdb. Otherwise, OJB_HOME/bin contains both build.sh and build.bat scripts that invoke Ant, distributed with the OJB package. (If you are not using Hypersonic, make sure your JDBC driver is in the classpath or that the .jar file is copied over to the OJB_HOME/lib directory.)

  4. You should see "BUILD SUCCESSFUL" at the end. Otherwise, check your database profile and the on-screen log messages.

2.4.3 The Test Application

JUnit is a good way to test your application and verify settings. This test application shows the OJB classes we use to persist and query objects. They are PersistenceBroker, Criteria, and Query. To run the application, you should have the OJB distribution .jar files in your CLASSPATH. In addition, the OJB.properties file and OJB XML descriptor files should also be in the CLASSPATH.

Example 12: Test Application

public class EmployeeTest extends TestCase
{
    private PersistenceBroker broker = null;

    public EmployeeTest(String arg0)
    {
        super(arg0);
        broker = PersistenceBrokerFactory.defaultPersistenceBroker();
    }

    public static void main(String[] args)
    {
        junit.textui.TestRunner.run(EmployeeTest.class);
    }

    public void testQuery() throws Exception
    {
        broker.beginTransaction();

        Manager manager = newManager();
        manager.setName("John Smith");
        manager.setProjectNumber(new Integer(10));

        broker.store(manager);

        Employee employee = newEmployee();
        employee.setName("David Cosby");

        broker.store(employee);

        broker.commitTransaction();

        Criteria crit = new Criteria();

        Query q = QueryFactory.newQuery(EmployeeImpl.class, crit);

        Collection results = broker.getCollectionByQuery(q);

        assertTrue(results.size() == 2);
    }

    private Employee newEmployee()
    {
        return new EmployeeImpl();
    }

    private Manager newManager()
    {
        return new ManagerImpl();
    }
}

That's it. With only a few lines of extra code, your business objects can be made persistent without having to be remodeled to entity beans.

3. Conclusion

Free and robust persistence frameworks are available today. They have good object-relational mapping capabilities, so that developers can work on the object models that are most natural for the business requirements. Given the complexities of today's business applications, the capabilities provided by these frameworks should not be overlooked.

4. Appendix

It is often desirable to decouple your application from the specifics of a persistence framework, so that you can easily switch frameworks in the future. I recommend creating custom PersistenceManager and Transaction classes for this purpose. For example:

Example 13: PersistenceManager and Transaction Interfaces

/**
 * The abstract base class for all PersistenceManager. It provides
 * method to persist and object and to obtain the current
 * transaction.
 */
public abstract class PersistenceManager
{
    public static PersistenceManager getInstance()
    {
        return new OJBPersistenceManager();
    }

    public abstract void makePersistent(Object o)
        throws PersistenceException;

    public abstract Transaction currentTransaction()
        throws PersistenceException;

}

/**
 * A Transaction interface. It provides minimal support for
 * beginning, committing, and rolling back a transaction.
 */
public interface Transaction
{
    public void begin() throws PersistenceException;

    public void commit() throws PersistenceException;

    public void rollback() throws PersistenceException;
}


/**
 * An OJB specific implementation of the PersistenceManager
 * class.
 */
public class OJBPersistenceManager extends PersistenceManager
{
    private PersistenceBroker broker = null;

    public OJBPersistenceManager()
    {
        broker = PersistenceBrokerFactory.defaultPersistenceBroker();
    }

    public void makePersistent(Object o) throws PersistenceException
    {
        try
        {
            broker.store(o);
        }
        catch (PersistenceBrokerException e)
        {
            // rethrow PersistenceException
        }
    }

    public Transaction currentTransaction() throws PersistenceException
    {
        return new OJBTransaction(broker);
    }
}

/**
 * An OJB specific implementation of the Transaction
 * interface.
 */
public class OJBTransaction implements Transaction
{
    private PersistenceBroker broker = null;

    public OJBTransaction(PersistenceBroker broker)
    {
        this.broker = broker;
    }

    public void begin() throws PersistenceException
    {
        try
        {
            broker.beginTransaction();
        }
        catch(TransactionAbortedException e)
        {
            // rethrow PersistenceException
        }
        catch(TransactionInProgressException e)
        {
            // rethrow PersistenceException
        }
    }

    public void commit() throws PersistenceException
    {
        try
        {
            broker.commitTransaction();
        }
        catch(TransactionAbortedException e)
        {
            // rethrow PersistenceException
        }
        catch(TransactionNotInProgressException e)
        {
            // rethrow PersistenceException
        }
    }

    public void rollback() throws PersistenceException
    {
        try
        {
            broker.abortTransaction();
        }
        catch(TransactionNotInProgressException e)
        {
            // rethrow PersistenceException
        }
    }
}

After the above classes are created, it is very simple to convert the test application to use them:

Example 14: Example Using PersistenceManager and Transaction Interfaces

PersistenceManager pm = PersistenceManager.getInstance();
Transaction tx = pm.currentTransaction();

tx.begin();
Manager manager = newManager();
manager.setName("John Smith");
manager.setProjectNumber(new Integer(10));

pm.makePersistent(manager);

Employee employee = newEmployee();
employee.setName("David Cosby");

pm.makePersistent(employee);

tx.commit();

It looks surprisingly like JDO, doesn't it? With a little more effort, you can also hide the OJB query mechanism.

Charles Chan is a senior software developer and consultant at Finetix LLC.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.