The Apache DB Project
ObJectRelationalBridge

OJB

Downloads

Documentation

Development

Translated (Web)

Using OJB as a persistence layer in your applications
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 src/test/org/apache/ojb/tutorial1/ directory.

This document explains the architecture of a simple application which uses the ObJectRelationalBridge PersistenceBroker API to implement five use cases which include persistence operations (retrieval, storage, and deletion of objects). This document will also demonstrate how a simple persistence object Product is mapped to a table in a relational database.

The Tutorial Application

The sample application is a console based data entry application -- a program to manipulate a product catalog database with five simple use cases:

  1. List all Products - display all products from the persistent store
  2. Enter a new Product - create a new product in the persistent store
  3. Edit an existing Product record - update a product in the persistent store
  4. Delete a Product record - remove a product from the persistent store
  5. Quit the application - for consistency we implement this as a use case

Only these five basic use cases have been implemented in the interest of keeping this tutorial focused on conveying only the basic concepts of OJB. This tutorial does not explain the necessary steps to model or implement 1:1, 1:n, or m:n mapping. For information about this please refer to Tutorial 3 Advanced O/R.

Running the Tutorial Application

To execute the application described in this tutorial, you must prepare the tutorials from the source distribution of OJB. This is accomplished via Ant build scripts which are executed by a script in the "bin" directory of the OJB distribution. (Note: This tutorial assumes that you have already downloaded the OJB source distribution.) There is no need to configure a database in order to run these applications. The tutorial applications ship with HSQLDB integration, all you need to do is run the prepare-tutorials Ant target.

Unpack the OJB source distribution, make sure that your current working directory is the top level directory of the OJB distribution and run the following script:

  • On Windows 9x/NT/2000/XP:
    • Prepare the Tutorials: bin\build.bat prepare-tutorials
    • Execute this Tutorial: bin\tutorial1.bat
  • On Unix/Linux:
    • Prepare the Tutorials: bin/build.sh prepare-tutorials
    • Execute this Tutorial: bin/tutorial1.sh
Tutorial Application Architecture

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. 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 main event loop. The program waits for user input in Application.selectUseCase(). Once a UseCase is selected, it is executed via the UseCase.apply() method. This UseCase class is a simple implementation of the well-known Command design pattern.

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

The UseCase interface contains two methods: getDescription() and apply(). UseCase.getDescription() is used to display a short description of the use case to the user. This method is used in Application.selectUseCase() to display each use case in a menu. UseCase.apply() performs the business logic implemented by the specific implementation of UseCase.

Starting with the simplest use case in our application - UCQuitApplication. Here is the apply() method from the quit application use case:

    public void apply()
    {
        System.out.println("bye...");
        System.exit(0);
    }
  
Using the OJB PersistenceBroker API in the UseCase Implementations

The code example above was trivial - there are no persistence operations involved in quitting the application. Let's move on to a more relevant example - UCListAllProducts. This use case must retreive a Collection containing all products from the database. It must then iterate over this collection and print each product.To retreive the collection from the database we need a method from the OJB API.

OJB provides three major APIs.

  • The PersistenceBroker
  • An ODMG implementation
  • A JDO implementation

In this first tutorial only the PersistenceBroker API is used as it is the most straightforward of the three options. Tutorial 2: Using the ODMG API and Tutorial 4: Using the JDO API will implement the same application using these different methods of database access.

You will find the source for the PersistenceBroker API in the package org.apache.ojb.broker. The key component in this package 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 PersistenceBroker instance, how to configure its O/R mapping repository and how to use the available functions.

Obtaining a Broker Instance

The Application class's public constructor demonstrates how to obtain a PersistenceBroker instance:

public Application()
{
    PersistenceBroker broker = null;
    try
    {
        broker = PersistenceBrokerFactory.
                    defaultPersistenceBroker();
    }
    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));
}
  

The PersistenceBrokerFactory creates an instance of PersistenceBroker using the resource repository.xml as a mapping repository (more details on this repository in the section on the Object/Relational mapping. The PersistenceBroker instance is passed to the constructor of the four UseCase objects which store this PersistenceBroker instance as a protected member variable.

Retrieving Collections and Iterators

The next thing we need to know is how to use this broker instance to implement our persistence operations. In this use case, we have to retrieve a collection containing all product entries from the database. To retrieve a collection containing objects matching some criteria we can use PersistenceBroker.getCollectionByQuery(Query query). Where Query is a class that allows specification of criteria such as price > 100 or userId == 3. In this case we want to select all of the persistence objects stored in the Product table. We need no (or null) filtering criteria. According to the ODMG the Collection containing all instances of a persistent class is called an Extent.

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

public void apply()
{
    System.out.println("The list of available products:");

    // build a query which selects all objects of class Product,
    // without any further criteria
    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();
    }
}
  

Imagine that there are 10,000 products in the product table: Retrieving a collection of all Products from the persistent store would be a very expensive operation - new objects are created and an entire table is loaded into memory. In this tutorial application, performance is not a major issue, but in a real-world application OJB provides a more efficient mechanism for iterating through all the available products. If your application does not need to maintain a collection of product objects in memory, you should call getIteratorByQuery() which returns an Iterator instead of a Collection.

This method is extremely useful if you write applications that need to iterate over large resultsets. Instances are not created all at once, they are created on an "as needed" basis as you iterate through the result. Instances of persistent objects which are no longer referenced by the application can be reclaimed by the garbage collector. Using this method the code would resemble the following:

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.
    Query query = new QueryByCriteria(Product.class, null);
    try
    {
        // ask the broker to retrieve an Iterator
        java.util.Iterator iter = broker.getIteratorByQuery(query);
        // 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 will 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 (product name, price and available stock). This data is stored in the new object's attributes. We then 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 an assignment to newProduct._id, the primary-key attribute. upon storing 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).

Updating Objects

When a user wants to edit a product ( by selecting item #2 from the menu ), the user must first enter the "id" of the product to be edited. Since the application does not maintain a list of product objects, the system must first retrieve a product from persistent storage via the PersistenceBroker.

Selecting an object from the PersistenceBroker is simple - we must first create a QueryByCriteria object. A QueryByCriteria object is created with a new Product object that contains an "id" property which has been populated with the id the user supplied. You will notice that all of the other properties of the Product object remain unpopulated. We have essentially defined a filter by populating only the "productId" property - our query will retrieve only the product that has this productId. Instead of passing an object to the query constructor, we could have passed a Class, and a Criteria object. Constructing a Criteria object directly allows you to specify an arbitrarily complex collection of query criteria such as "productId" must be greater than 2 and less than 5. A complex query is demonstrated later in this tutorial.

The Product object is retrieved, with the call to broker.getObjectByQuery(query). This retreived object is then edited by setting properties based on the user input, and this object is then stored in the persistence storage through a call to broker.store(toBeEdited). Here is the code from UCEditProduct.

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

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

    // 2. build a QueryByCriteria from this sample instance:
    Query query = new QueryByCriteria(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 product's unique id and the PersistenceBroker 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 do not have a reference to the selected Product.
    // So first we have to lookup the object,
    // we do this by a query by Criteria
    // 1. build an example object with matching primary key values:
    Product example = new Product();
    example.setId(id);
    // 2. build a QueryByCriteria from this sample instance:
    Query query = new QueryByCriteria(example);
    try
    {
        // start broker transaction
        broker.beginTransaction();
        // lookup the product specified by the Criteria
        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();
    }
}
  

A QueryByCriteria was used to simplify this tutorial and reduce the amount of code necessary to demonstrate the essential components of OJB. It is important to note that one can also build a query based on filter criteria through the Criteria object. The following code demonstrates the construction of a same query using a Criteria object:

    // build filter criteria:
    Criteria criteria = new 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);
    ...
  

An arbitrary number of criteria can be added in order to produce very complex queries. The following code demonstrates a more complex use of the Criteria object, one that is not present in this tutorial application. This code retrieves all products which are priced less than 5.40 (USD, Yen, Euros, etc.) and of which we have at least 2 million in stock:

    // build filter criteria:
    Criteria criteria = new Criteria();
    criteria.addLessThan("price", new Double( 5.40 ));
    criteria.addGreaterThan("stock", new Integer( 2000000 ));
    // build a query for the class Product with these filter criteria:
    Query query = new QueryByCriteria(Product.class, criteria);
    ...
  
Completing the Tutorial Application

The completion of this tutorial program is left as an exercise for the tutorial reader.

Now you are familiar with the basic functionalities of the OJB PersistenceBroker API. 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 (bin\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 org.apache.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 modifying compiled classes
  • Mapping can be inspected and changed at runtime, allowing 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 either Java Reflection or JavaBeans compliant access to inspect and modify business objects. OJB takes great care to reduce the dynamic access overhead to a minimum.

The following sections dmeonstrate the O/R mapping for the tutorial application.

A Persistent Class: Product

There is only one persistent class in the tutorial application, the class Product. Here its' definition:

    package org.apache.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;

        ...
    }
  

The method definitions have been ommitted, as they are not relevant for the O/R mapping process.

The Product Database Table

Now, take a look at a corresponding table definition, in SQL DDL (The InstantDB syntax is given here, the syntax may vary slightly for your favourite RDBMS):

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

Notice that the 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, or make use an anonymous key. This example allows the primary key to intrude into the object 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 no-argument constructor, but it may be private.

The Product 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"?>
<!-- This is a sample metadata repository for the ObJectBridge System.
     Use this file as a template for building your own mappings-->

<!-- defining entities for include-files -->
<!DOCTYPE descriptor-repository SYSTEM "repository.dtd" [
<!ENTITY user SYSTEM "repository_user.xml">
<!ENTITY junit SYSTEM "repository_junit.xml">
<!ENTITY internal SYSTEM "repository_internal.xml">
]>
<!DOCTYPE descriptor-repository PUBLIC
       "-//Apache Software Foundation//DTD OJB Repository//EN"
       "repository.dtd"
[
<!ENTITY database SYSTEM "repository_database.xml">
<!ENTITY internal SYSTEM "repository_internal.xml">
<!ENTITY junit SYSTEM "repository_junit.xml">
<!ENTITY user SYSTEM "repository_user.xml">
<!ENTITY ejb SYSTEM "repository_ejb.xml">
<!ENTITY jdo SYSTEM "repository_jdo.xml">
]>


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

    <!-- include all used database connections -->
    &database;

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

    <!-- include mappings for JUnit tests -->
    <!-- This could be removed (with <!ENTITY entry),
         if junit test suite was not used
    -->
    &junit;

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

    <!-- include mappings for the EJB-examples -->
    <!-- &ejb; -->

    <!-- include mappings for the JDO tutorials -->
    &jdo;

</descriptor-repository>

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.
    More details about the repository grammar you can find here.
  2. The mapping contains a default JDBCConnectionDescriptor. Such a Descriptor contains information about the JDBC connection used for persistence operations. The JDBC connection defined by this default descriptor is used for all classes that do not have a specific JDBCConnectionDescriptor.
    In our example the Descriptor specifies that all operations have to use the HsqlDb JDBC driver and that the database is located in the samples directory.
  3. Mappings for OJB regression tests. This is done by including the file repository_junit.xml by means of the XML entity &junit;. This entity is defined as a file include:
    	<!ENTITY junit SYSTEM "repository_junit.xml">
    	
  4. The OJB internal mappings. OJB needs some internal tables, e.g for maintaining 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 must not be modified. More information on the internal tables can be found here.
    These internal mappings are defined by including the file repository_internal.xml by means of the XML entity &internal;.
  5. The user defined mappings. User defined mappings contain all object/relational mapping information on your persistent classes. By default the mappings for the tutorial applications are stored here. These mappings are defined by including the file repository_user.xml by means of the XML entity &user;.
    All mappings defined by you should be placed in the file repository_user.xml only!

Now we have a look at the mapping information for the Product class. I placed this mapping into the file repository_user.xml to make it easy to find. Quite at the top of this file there is a class-descriptor definition for the Product class:

   <!-- Definitions for org.apache.ojb.broker.Product -->
   <class-descriptor
      class="org.apache.ojb.broker.Product"
      table="PRODUCT"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="name"
         column="NAME"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="price"
         column="PRICE"
         jdbc-type="DOUBLE"
      />
      <field-descriptor
         name="stock"
         column="STOCK"
         jdbc-type="INTEGER"
      />
   </class-descriptor>

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

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

field-descriptor are sufficient for attributes with primitive data types and their respective wrapper classes.

If your persistent class contains reference attributes pointing to other persistent classes, you have to use reference-descriptor-elements to describe their behaviour. If the persistent class has array- or collection-attributes containing persistent objects you have to use collection-descriptor elements. You will find several examples for such classes in the package org.apache.ojb.broker and their corresponding descriptors in the sample repository.xml. There is a separate tutorial on these more advanced mapping topic.

A complete documentation of the repository syntax and semantics can be found here.

There are several tools available that can assist you in building the mapping file. Please have a look at this document to learn more about those tools.

Conclusion

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

there are three additional tutorials on using the OJB ODMG API, on using the OJB JDO 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.


Copyright © 1999-2003, Apache Software Foundation