ObJectRelationalBridge
BRIDGING JAVA OBJECTS AND RELATIONAL DATABASES


ObJectRelationalBridge Tutorial - Part 1

Using OJB as a persistence layer in your applications

Author: Thomas Mahler, march 2001

Introduction

This document demonstrates how to use the ObJectRelationalBridge (OJB) object/relational mapping in a simple application scenario. The tutorial application implements a product catalog database with some basic use cases. The source code for the tutorial application is shipped with the OJB source distribution and resides in the package test.ojb.tutorial1.

I begin with explaining the application and outlining its architecture. After that I will explain how the ObJectRelationalBridge API can be used in the implementations of the use cases to perform persistence operations like retrieval, storage and deletion of objects. Finally I will show how the O/R mapping is built for the applications persistent class Product.

The sample application

The sample application is a console based data entry application. It's a product catalog database with simple use cases:

1.      List all Products in catalog (all Products entered in this or earlier sessions and did not get deleted must be displayed)

2.      Enter a new Product (the new Product must be made persistent)

3.      Edit an existing Product record

4.      Delete a Product record (the Product must be removed from the persistent store)

5.      To keep a homogenous design quitting the application is also modelled as a use case.

These use case shall implement only a rudimentary functionality. Thus we don't have to care about special cases or variants.

see it running...

To see the tutorial application at work you first have to compile the sources by executing build[.sh] main in the ojb toplevel directory.
Next you have to setup the demo database by executing build[.sh] tests.

Now you can start the tutorial application by executing tutorial1[.sh] in the ojb toplevel directory.

Architectural scetch

The overall architecture of the application is outlined in the class diagram below.

The Application maintains a number of UseCases. The interface UseCase is implemented by an abstract base class AbstractUseCase that provides some convenience code useful for all concrete implementations. There are four concrete UseCase Implementations: UCListAllProducts, UCEnterNewProduct, UCDeleteProduct and UCQuitApplication. These concrete classes implement the functionality of the above mentioned Use Cases. The class Product represents a product entry in the product database and is the only persistent class in this application.





figure 1: Class Diagram with the application classes of the tutorial application

The Application::run() method runs a non-terminating driver loop. It does the following: let the user select a use case and perform this use case and so forth:

        while (true)
        {
                try
                {
                        // select a use case and perform it
                        UseCase uc = selectUseCase();
                        uc.apply();
                }
                catch (Throwable t)
                {
                        System.out.println(t.getMessage());
                }
        }

A UseCase must implement two methods: getDescription() and apply(). UseCase::getDescription() is used to display a short information on the use case. This method is used in Application::selectUseCase() to display each use case. UseCase::apply() performs the business logic implemented by the use case.

Let's start with the most simple use case: UCQuitApplication. Here is its apply() method:

public void apply()
{
        System.out.println("bye...");
        System.exit(0);
}

Using the OJB PersistenceBroker API in the UseCase implementations

Ok, that was rather trivial as no persistence operations were involved. Now to some real work: UCListAllProducts. This use case must lookup a Collection containing all products from the persistent storage. Then it must iterate over this collection and print each product.
To lookup the collection from the persistent storage we need a method from the OJB API. OJB provides two major APIs. The PersistenceBroker and the ODMG implementation. In this first tutorial I will only use the PersistenceBroker API as it is simpler to use. Using the ODMG API is covered in a separate tutorial.

You will find the source for the PersistenceBroker API in the package ojb.broker. The JavaDoc is here. The Most important part of this API is the interface PersistenceBroker. It provides methods for retrieval, storage and deletion of objects. To use it in your application you must know how to retrieve a Broker instance, how to configure it’s O/R mapping repository and how to use its API.

Obtaining a broker instance

To learn how to obtain a Broker instance let’s have a look at the Application class. The public constructor of this class looks like follows:

public Application()
{
        PersistenceBroker broker = null;
        try
        {
                broker = PersistenceBrokerFactory.createPersistenceBroker("repository.xml");
        }
        catch (Throwable t)
        {
                t.printStackTrace();
        }
        
        useCases = new Vector();
        useCases.add(new UCListAllProducts(broker));
        useCases.add(new UCEnterNewProduct(broker));
        useCases.add(new UCDeleteProduct(broker));
        useCases.add(new UCQuitApplication(broker));
}

We just ask the PersistenceBrokerFactory to create an instance that uses the file ./repository.xml as mapping repository (more details on this repository in the section on the Object /Relational mapping). This broker instance is reached to the constructors of the UseCases. The constructors just store it in a protected attribute broker for further usage.

Retrieving collections and iterators

The next thing we need to know is how to use this broker instance to help us in our persistence operations. In this use case we have to retrieve a collection containing all product entries from the persistent store. To retrieve a collection containing objects matching some criteria we can use PersistenceBroker:: getCollectionByQuery(Query query). Where Query is a Class that allows to formulate criteria like “price > 100”. In our case we want to select all persistent instances, so we need no filtering criteria, they can thus be left null.

Here is the code of  the UCListAllProducts::apply() method:

public void apply()
{
        System.out.println("The list of available products:");
        // build a query that selects all objects of Class Product, without any further criteria
        // according to ODMG the Collection containing all instances of a persistent class is called "Extent"
        Query query = new QueryByCriteria(Product.class, null);
        try
        {
                // ask the broker to retrieve the Extent collection
                Collection allProducts = broker.getCollectionByQuery(query);
                // now iterate over the result to print each product
                java.util.Iterator iter = allProducts.iterator();
                while (iter.hasNext())
                {
                        System.out.println(iter.next());
                }
        }
        catch (Throwable t)
        {
            t.printStackTrace();
        }
}

If you don’t need the resulting collection for further reference as in this example where we just want to iterate over all products once, it may be a good idea not to use getCollectionByQuery(…) but getIteratorByQuery(…) which returns an Iterator.

This method is extremely useful if you write applications that have to iterate over large resultsets. Instances are not created all at once but only on demand, and instances that are not longer referenced by the application my be reclaimed by the garbage collector. Using this method the code will look like follows:

public void apply()
{
        System.out.println("The list of available products:");
        // build a query that select all objects of Class Product, without any further criteria
        // according to ODMG the Collection containing all instances of a persistent class is called "Extent"
        Query query = new QueryByCriteria(Product.class, null);
        try
        {
                // ask the broker to retrieve an Iterator
                               java.util.Iterator iter = broker.getIteratorB
                // now iterate over the result to print each product
                while (iter.hasNext())
                {
                        System.out.println(iter.next());
                }
        }
        catch (Throwable t)
        {
            t.printStackTrace();
        }
}
 
For further information you may have a look at the PersistenceBroker JavaDoc and at the Query JavaDoc.

Storing objects

Now we’ll have a look at the use case UCEnterNewProduct. It works as follows: first create a new object, then ask the user for the new product’s data (productname, price and available stock). These data is stored in the new objects attributes. Then we must store the newly created object in the persistent store. We can use the method PersistenceBroker::store(Object obj) for this task.

public void apply()
{
        // this will be our new object
        Product newProduct = new Product();
        // now read in all relevant information and fill the new object:
        System.out.println("please enter a new product");
        String in = readLineWithMessage("enter name:");
        newProduct.setName(in);
        in = readLineWithMessage("enter price:");
        newProduct.setPrice(Double.parseDouble(in));
        in = readLineWithMessage("enter available stock:");
        newProduct.setStock(Integer.parseInt(in));
        
        // now perform persistence operations
        try
        {
                // 1. open transaction
                broker.beginTransaction();
  
                // 2. make the new object persistent
                broker.store(newProduct);
                broker.commitTransaction();
        }
        catch (PersistenceBrokerException ex)
        {
                // if something went wrong: rollback
                broker.abortTransaction();
                System.out.println(ex.getMessage());
                ex.printStackTrace();
        }
}
 

Maybe you have noticed that there has not been any assignment to newProduct._id, the primary key attribute. On storing of newProduct OJB detects that the attribute is not properly set and assigns a unique id. This automatic assignment of unique Ids for the Attribute _id has been eplicitly declared in the XML-Repository (see section 'Defining the Object/Relational Mapping' below).



Updating Objects

Editing and updating a product entry works quite similar. The user enters the products unique id and the broker tries to lookup the respective object. This lookup is necessary as our application does not hold a list of all products. The found product is then edited and then stored. The PersistenceBroker uses the .store(...) method for inserting new objects as well as for updating existing objects. Here is the code:

    public void apply()
    {
        String in = readLineWithMessage("Edit Product with id:");
        int id = Integer.parseInt(in);

        // We don't have a reference to the selected Product.
        // So first we have to lookup the object,
        // we do this by a query by example (QBE):
        // 1. build an example object with matching primary key values:
        Product example = new Product();
        example.setId(id);

        // 2. build a QueryByExample from this sample instance:
        Query query = new QueryByExample(example);
        try
        {
            // 3. start broker transaction
            broker.beginTransaction();

            // 4. lookup the product specified by the QBE
            Product toBeEdited = (Product) broker.getObjectByQuery(query);

            // 5. edit the existing entry
            System.out.println("please edit the product entry");
            in = readLineWithMessage("enter name (was " + toBeEdited.getName() + "):");
            toBeEdited.setName(in);
            in = readLineWithMessage("enter price (was " + toBeEdited.getPrice() + "):");
            toBeEdited.setPrice(Double.parseDouble(in));
            in = readLineWithMessage("enter available stock (was " + toBeEdited.getStock()+ "):");
            toBeEdited.setStock(Integer.parseInt(in));



            // 6. now ask broker to store the edited object
            broker.store(toBeEdited);
            // 7. commit transaction
            broker.commitTransaction();
        }
        catch (Throwable t)
        {
            // rollback in case of errors
            broker.abortTransaction();
            t.printStackTrace();
        }
    }



Deleting Objects

 The UseCase UCDeleteProduct allows the user to select one of the existing products and to delete it from the persistent storage. The user enters the products unique id and the broker tries to lookup the respective object. This lookup is necessary as our application does not hold a list of all products. The found object must then be deleted by the broker. Here is the code:

public void apply()
{
        String in = readLineWithMessage("Delete Product with id:");
        int id = Integer.parseInt(in);

        // We don't have a reference to the selected Product.
        // So first we have to lookup the object,
        // we do this by a query by example (QBE):
        // 1. build an example object with matching primary key values:
        Product example = new Product();
        example.setId(id);
        // 2. build a QueryByExample from this sample instance:
        Query query = new QueryByExample(example);
        try
        {
                // start broker transaction
                broker.beginTransaction();
                // lookup the product specified by the QBE
                Product toBeDeleted = (Product) broker.getObjectByQuery(query);
                // now ask broker to delete the object
                broker.delete(toBeDeleted);
                // commit transaction
                broker.commitTransaction();
        }
        catch (Throwable t)
        {
                // rollback in case of errors
                broker.abortTransaction();
                t.printStackTrace();
        }
}

I did use a QueryByExample in this case as it needs a minimum of code for primary key lookups. But you can also build a query based on filter criteria. In this use case the query building would look like follows:

        // build filter criteria:
        ojb.broker.query.Criteria criteria = new ojb.broker.query.Criteria();
        criteria.addEqualTo(“_id”, new Integer(id));
        // build a query for the class Product with these filter criteria:
        Query query = new QueryByCriteria(Product.class, criteria); 
        ...
 

Finishing this application is left as an exercise to the interested reader…

Now you are familiar with the basic functionalities of the OJB PersistenceBroker. To learn more you might consider implementing the following additional use cases:

1.      List all products with a price > 1000 (or let the user enter a criteria)

2.      Delete all products that have a stock of  0

3.      increase the price of all products that cost less then 500 by 11% (and make the changes persistent)

 

Defining the Object/Relational Mapping

After looking at the code of the tutorial application and at the sample database (build browse_db will start a browser on the InstantDB database) you will probably ask: “How is it possible for the OJB Broker to store objects of class Product in the table PRODUCT without any traces in the sourcecode?” or “How does OJB know that the database column ‘NAME’ is mapped onto the attribute name?”.

The answer is: It's done in the OJB Metadata Repository. This Repository consists of a set of classes describing the O/R mapping (have a look at the package ojb.broker.metadata). The repository consists of ordinary Java objects that can be created and modified at runtime. This brings a lot flexibility for special situations where it is neccessary to change the mapping dynamically.Keeping the mapping dynamic has several advantages:

·        No preprocessing of Java sourcecode, no fix compiled persistence classes

·        Mapping can be inspected and changed at runtime, allows maximum flexibility in changing persistence behaviour or in building your own persistence layers on top of OJB.

But it has also at least one disadvantage: Performance. Due to the dynamic approach OJB uses the slow Java Reflection API to inspect and modify business objects. But we took great care in reducing the reflection overhead to a minimum.

In the following sections I will show how the O/R mapping is defined for the tutorial application.

 

The persistent class Product

There is only one persistent class in our tutorial application, the class Product. Here it’s definition:

package test.ojb.tutorial1;
/** 
 * represents product objects in the tutorial system
 */
public class Product 
{
    /** product name*/
    protected String name;
    /** price per item*/
    protected double price; 
    /** stock of currently available items*/
    protected int stock; 
 
    ...
}

I ommited the method definitions, as they are not relevant for the O/R mapping process.

The corresponding database table

Now we take a look at a corresponding table definition, in SQL DDL (I give the instantDB syntax here, the syntax may vary slightly for your favourite RDBMS):

CREATE TABLE PRODUCT (
  ID    INT PRIMARY KEY,
  NAME  CHAR(100),
  PRICE DOUBLE,
  STOCK INT
)

You will notice that I added a primary key column »ID«. This is an »artificial attribute« as it not derived from the domain model. To use such an artificial key instead of a compound key of domain attributes is recommended but not mandatory for O/R mappings. If you are using such artificial keys you have to modify the original class layout slightly and include a corresponding attribute. OJB requires that all primary key columns of a table are reflected in attributes in the coresponding class. This is one of the very few »intrusions« of the persistence layer into your business code, because these attributes are not necessarily part of the OOD model.

So we must change our initial class definition slightly to match this requirement:

public class Product 
{
    /** this is the primary key attribute needed by OJB to identify instances*/
    private int _id;
 
    /** product name*/
    protected String name;
    /** price per item*/
    protected double price;
    /** stock of currently available items*/
    protected int stock; 
}

Apart from the primary key attribute there is no further instrusion of persistence code into our business object. No need to extend a base class or to implement any interfaces. That's why we claim that OJB provides »transparent persistence«.

There is one important exception: persistent capable classes must provide a public default constructor. Implementing a constructor that initializes all persistent attributes is recommended for performance reasons but not required. OJB will print out warnings when such a constructor is missing. When you run the tutorial application you will see such a warning as the class Product does not provide an initializing constructor.

The Mapping

Now we have to describe the mapping from the class Product to the database table PRODUCT. This is typically not done programmatically but by declaration in a repository xml file. The DescriptorRepository class provides factory methods to boot itself from this XML file. The resulting repository can be manipulated programmaticaly later. (It’s also possible to build up a complete repository programmatically.)

We have to write our own mapping and integrate it into the OJB sample repository in src/test/ojb/repository.xml. This XML file looks like follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE MappingRepository SYSTEM "repository.dtd">
<!--This is a sample metadata repository for the ObJectRelationalBridge System.
It maps some demo classes to a instantdb database, which is part of
this demo.
Use this file as a template for building your own mappings-->
 
<MappingRepository>
  <!-- The Default JDBC Connection. If a Class does not specify an own JDBC Connection,
       the Connection specified here will be used. -->
  <JdbcConnectionDescriptor id="default">
    <dbms.name>InstantDB</dbms.name>
    <driver.name>org.enhydra.instantdb.jdbc.idbDriver</driver.name>
    <url.protocol>jdbc</url.protocol>
    <url.subprotocol>idb</url.subprotocol>
    <url.dbalias>..\\setup\\demo.prp</url.dbalias>
  </JdbcConnectionDescriptor>
 
 
  <!-- OJB INTERNAL MAPPINGS START HERE, DO NOT EDIT -->
  ...
  ...
  ...
  <!-- END OF OJB INTERNAL MAPPINGS-->
</MappingRepository>

 

This file contains a lot of information:

1.      the XML file is validated against the DTD »repository.dtd«. This enforces syntactical correctness of the xml file. Be sure to keep the dtd file alway in the same directory as the xml file, otherwise the XML parser will complain that it could not find the DTD.

2.      The mapping contains already a default JDBCConnectionDescriptor. Such a Descriptor contains information about JDBC connections used for persistence operations. OJB allows to have one JDBCConnectionDescriptor per class. But it is also possible to define a default Descriptor. The JDBC connection defined by this descriptor is used for all classes that do not have a specific JDBCConnectionDescriptor.
In our example the Descriptor specifies that by default all operations have to use the InstantDB JDBC driver and that the database demo.prp in the samples directory has to be used.

3.      Mappings for OJB regression tests and sample applications

4.      The OJB internal mappings. OJB needs some internal tables, e.g for mainatining locks, Auto counters and the ODMG collections and Maps. The corresponding mappings are contained here. They are essential for the proper operation of the system and may not be modified

Now we have a look at the mapping information for the Product class in it’s own ClassDescriptor section:

 

<!-- Definitions for test.ojb.tutorial1.Product -->
<ClassDescriptor id="100">
  <class.name>tutorial.Product</class.name>
  <table.name>PRODUCT</table.name>
  <FieldDescriptor id="1">
    <field.name>_id</field.name>
    <column.name>ID</column.name>
    <jdbc_type>INT</jdbc_type>
    <PrimaryKey>true</PrimaryKey>
    <autoincrement>true</autoincrement>
  </FieldDescriptor>
  <FieldDescriptor id="2">
    <field.name>name</field.name>
    <column.name>NAME</column.name>
    <jdbc_type>CHAR</jdbc_type>
  </FieldDescriptor>
  <FieldDescriptor id="3">
    <field.name>price</field.name>
    <column.name>PRICE</column.name>
    <jdbc_type>DOUBLE</jdbc_type>
  </FieldDescriptor>
  <FieldDescriptor id="4">
    <field.name>stock</field.name>
    <column.name>STOCK</column.name>
    <jdbc_type>INT</jdbc_type>
  </FieldDescriptor>
</ClassDescriptor>

 

A ClassDescriptor defines how a given class is mapped onto a RDBMS table.

For each persistent attribute of this class we need a FieldDescriptor, that defines the column where this attribute has to be stored. For primary key attributes we have a special marker element. The primary key attribute is also tagged as „autoincrement“, this will tell OJB to assign unique Ids to this attribute. Internally this is done by a SequenceManager utility class.

FieldDescriptors are sufficient for attributes with primitive data types (and Strings).

If your persistent class contains reference attributes to other user defined classes, arrays or collections, you must describe their behaviour in ObjectReferenceDescriptor- or CollectionDescriptor elements. You will find several examples for such classes in the package test.ojb.broker and their corresponding descriptors in the sample repository.xml. I will also provide a separate tutorial on this more advanced topic.

Tool support

Generally speaking there are three approaches in building an O/R mapping:

1.      Forward engineering, Java classes are given, SQL DDL and mapping has to be generated

2.      Reverse engineering, SQL DDL or a life Database are given, Java classes and mapping has to be generated

3.      Mapping proper, Java Classes and SQL DDL are given, Mapping has to be generated

Currently OJB does not provide automated tools for this tasks. But they are on our to-do list.

Conclusion

In this tutorial you learned to build a OJB Object/Relational mapping for a simple class layout and to use the the OJB PersistenceBroker API for persistence operations on instances of this class.

there are two additional tutorials on using the OJB ODMG API and on building advanced O/R mappings (including 1-1, 1-n relations, proxy techniques, supporting polymorphism and mapping inheritance hierarchies).

I hope you found this tutorial helpful. Any comments are welcome.


release: 0.8.375, date: 2002-04-04