ObJectRelationalBridge
BRIDGING JAVA OBJECTS AND RELATIONAL DATABASES


ObJectRelationalBridge Tutorial Part 3:

Advanced Object Relational Mapping techniques

Author: Thomas Mahler, september 2001

introduction

This tutorial presents some of the more advanced techniques related to O/R mapping with OJB. It is not organized as one large example but rather as a loose collection of code and mapping examples from the OJB regression test suite.

Throughout this tutorial I will use classes from the package test.ojb.broker. This is working code from the JUnit Testsuite. Thus it's guaranteed to work. It should be quite straightforward to reuse these samples to build up your own applications. I hope you'll find this hands on approach helpful for building your own OJB based applications.


table of contents

mapping 1:1 associations

mapping 1:n associations

types allowed for implementing 1:n associations

mapping m:n associations

Manual decomposition into two 1:n associations
Support for non-decomposed m:n mappings

setting load-, update- and delete-cascading

using proxy classes

using dynamic proxies
using a single proxy for a whole collection
using a proxy for a reference

type and value conversions

extents and polymorphism

polymorphism
extents

mapping inheritance hierarchies

mapping all classes on the same table
mapping each class on a distinct table
mapping classes on multiple joined tables

using rowreaders

rowreader example

mapping 1:1 associations

As a sample for a simple association we take the reference from an article to its productgroup.
This association is navigable only from the article to its productgroup. Both classes are modelled in the following class diagram. This diagram does not show methods, as only attributes are relevant for the O/R mapping process.





The association is implemented by the attribute productGroup. To automatically maintain this reference OJB relies on foreignkey attributes. The foreign key containing the groupId of the referenced productgroup is stored in the attribute productGroupId.

This is the DDL of the underlying tables:

CREATE TABLE Artikel
(
    Artikel_Nr         INT NOT NULL PRIMARY KEY,
    Artikelname        VARCHAR(60),
    Lieferanten_Nr     INT,
    Kategorie_Nr       INT,
    Liefereinheit      VARCHAR(30),
    Einzelpreis        FLOAT,
    Lagerbestand       INT,
    BestellteEinheiten INT,
    MindestBestand     INT,
    Auslaufartikel     INT
)

CREATE TABLE Kategorien
(
    Kategorie_Nr       INT NOT NULL PRIMARY KEY,
    KategorieName      VARCHAR(20),
    Beschreibung       VARCHAR(60)
)

To declare the foreign key mechanics of this reference attribute we have to add a ReferenceDescriptor to the ClassDescriptor of the Article class. This descriptor contains the following information:

  1. The attribute implementing the association (<rdfield>) is productGroup.

  2. The referenced object is of type (<referenced.class>) test.ojb.broker.ProductGroup.

  3. The tag <fk_descriptor_ids> contains a list of ids of those descriptors describing the foreignkey fields. In this case it only contains the id 4. The FieldDescriptor with the id 4 describes the foreignkey attribute productGroupId. Multiple foreignkey attributes may be separated by blanks.

See the following extract from the repository.xml file containing the Article ClassDescriptor:

   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Article</class.name>
      <table.name>Artikel</table.name>
      <FieldDescriptor id="1">
         <field.name>articleId</field.name>
         <column.name>Artikel_Nr</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
      </FieldDescriptor>
      <FieldDescriptor id="2">
         <field.name>articleName</field.name>
         <column.name>Artikelname</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <FieldDescriptor id="3">
         <field.name>supplierId</field.name>
         <column.name>Lieferanten_Nr</column.name>
         <jdbc_type>INTEGER</jdbc_type>
      </FieldDescriptor>
      <FieldDescriptor id="4">
         <field.name>productGroupId</field.name>
         <column.name>Kategorie_Nr</column.name>
         <jdbc_type>INTEGER</jdbc_type>
      </FieldDescriptor>
      ...
      <ReferenceDescriptor id="1">
         <rdfield.name>productGroup</rdfield.name>
         <referenced.class>test.ojb.broker.ProductGroup</referenced.class>
         <fk_descriptor_ids>4</fk_descriptor_ids>
      </ReferenceDescriptor>
   </ClassDescriptor>

This example does provide unidirectional navigation only. To provide bidirectional navigation by adding a reference from a ProductGroup to a single Article (say a sample article for the productgroup) we have to perform the following steps:

  1. Add an attribute private Article sampleArticle to the class ProductGroup.

  2. Add a foreignkey attribute private int sampleArticleId to ProductGroup.

  3. Add a column SAMPLE_ARTICLE_ID INT to the table Kategorien.

  4. Add a FieldDescriptor for the foreignkey attribute to the ClassDescriptor of the Class ProductGroup:

      <FieldDescriptor id="17">
         <field.name>sampleArticleId</field.name>
         <column.name>SAMPLE_ARTICLE_ID</column.name>
         <jdbc_type>INTEGER</jdbc_type>
      </FieldDescriptor>
  1. Add a ReferenceDescriptor to the ClassDescriptor of the Class ProductGroup:

      <ReferenceDescriptor id="1">
         <rdfield.name>sampleArticle</rdfield.name>
         <referenced.class>test.ojb.broker.Article</referenced.class>
         <fk_descriptor_ids>17</fk_descriptor_ids>
      </ReferenceDescriptor>

mapping 1:n associations

As a sample for a 1:n association we take the a different perspective on the previous example. We allow to have a ProductGroup multiple Articles. This association is navigable only from the productgroup to its articles. Both classes are modelled in the following class diagram. This diagram does not show methods, as only attributes are relevant for the O/R mapping process.




The association is implemented by the Vector attribute allArticlesInGroup. As in the previous example the Article class contains a foreignkey attribute productGroupId that identifies an articles productgroup. The Database table are the same as above.

To declare the foreign key mechanics of this collection attribute we have to add a CollectionDescriptor to the ClassDescriptor of the ProductGoup class. This descriptor contains the following information:

  1. the attribute implementing the association (<cdfield>) (in our case the attribute Vector allArticlesInGroup).

  2. The associated class (<items.class>) (in our case the Class Article).

  3. The ids of FieldDescriptors of the associated class describing the foreign key attributes. (<inverse_fk_descriptor_ids>) (in our case its again the FieldDescriptor for the attribute productGoupId with ID 4).

See the following extract from the repository.xml file containing the ProductGoup ClassDescriptor:

<!-- Definitions for test.ojb.broker.ProductGroup -->
   <ClassDescriptor id="2">
      <class.name>test.ojb.broker.ProductGroup</class.name>
      <table.name>Kategorien</table.name>
      <FieldDescriptor id="1">
         <field.name>groupId</field.name>
         <column.name>Kategorie_Nr</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
         <autoincrement>true</autoincrement>
      </FieldDescriptor>
      ...
      <CollectionDescriptor id="1">
         <cdfield.name>allArticlesInGroup</cdfield.name>
         <items.class>test.ojb.broker.Article</items.class>
         <inverse_fk_descriptor_ids>4</inverse_fk_descriptor_ids>
      </CollectionDescriptor>
   </ClassDescriptor>


With the mapping shown above OJB has two possibilities to load the Articles belonging to a ProductGroup:

  1. loading all Articles of the ProductGroup immediately after loading the ProductGroup. This is done with two SQL-calls: one for the ProductGroup and one for all Articles.
  2. if Article is a proxy (using proxy classes), OJB will only load the keys of the Articles after the ProductGroup. When accessing an Article-proxy OJB will have to materialize it with another SQL-Call. Loading the ProductGroup and all it's Articles will thus produce n+2 SQL-calls: one for the ProductGroup, one for keys of the Articles and one for each Article.
Both approaches have their benefits and drawbacks:

A. is suitable for a small number of related objects that are easily instantiated. It's efficient regarding DB-calls. The major drawback is the amount of data loaded. For example to show a list of ProductGroups the Articles may not be needed.

B. is best used for a large number of related heavy objects. This solution loads the objects when they are needed ("lazy loading"). The price to pay is a DB-call for each object.

Further down a third solution using a single proxy for a whole collection will be presented to circumvent the described drawbacks.

types allowed for implementing 1:n associations

OJB supports different types to implement 1:n associations. OJB detects the used type automatically, so there is no need to declare it in the repository file. There is also no additional programming required. The following types are supported:

  1. java.util.Collection, java.util.List, java.util.Vector as in the example above. Internally OJB uses java.util.Vector to implement collections.

  2. Arrays (see the file ProductGroupWithArray).

  3. User-defined collections (see the file ProductGroupWithTypedCollection). A typical application for this approach are typed Collections.
    Here is some sample code from the Collection class ArticleCollection. This Collection is typed, i.e. It accepts only InterfaceArticle objects for adding and will return InterfaceArticle objects with get(int index). To let OJB handle such a user-defined Collection it must implement the callback interface ManageableCollection. This interface provides hooks that are called by OJB during object materialization, updating and deletion.

public class ArticleCollection implements ManageableCollection, java.io.Serializable
{
    private Vector elements;

    public ArticleCollection()
    {
        super();
        elements = new Vector();
    }

    public void add(InterfaceArticle article)
    {
        elements.add(article);
    }

    public InterfaceArticle get(int index)
    {
        return (InterfaceArticle) elements.get(index);
    }

    /**
     * add a single Object to the Collection. This method is used during reading
     * Collection elements from the database. Thus it is is save to cast anObject
     * to the underlying element type of the collection.
     */
    public void ojbAdd(java.lang.Object anObject)
    {
        elements.add((InterfaceArticle) anObject);
    }

    /**
     * adds a Collection to this collection. Used in reading Extents from the Database.
     * Thus it is save to cast otherCollection to this.getClass().
     */
    public void ojbAddAll(ojb.broker.ManageableCollection otherCollection)
    {
        elements.addAll(((ArticleCollection) otherCollection).elements);
    }

    /**
     * returns an Iterator over all elements in the collection.
     * Used during store and delete Operations.
     */
    public java.util.Iterator ojbIterator()
    {
        return elements.iterator();
    }
}

mapping m:n associations

OJB provides support for manually decomposed m:n associations as well as an automated support for non decomposed m:n associations.

Manual decomposition into two 1:n associations

Have a look at the following class diagram:




We see a two classes with a m:n association. A Person can work for an arbitrary number of Projects. A Project may have any number of Persons associated to it.
Relational databases don't support m:n associations. They require to perform a manual decomposition by means of an intermediary table. The DDL looks like follows:

CREATE TABLE PERSON (
    ID          INT NOT NULL PRIMARY KEY,
    FIRSTNAME   VARCHAR(50),
    LASTNAME    VARCHAR(50)
  );

CREATE TABLE PROJECT (
    ID          INT NOT NULL PRIMARY KEY,
    TITLE       VARCHAR(50),
    DESCRIPTION VARCHAR(250)
  );

CREATE TABLE PERSON_PROJECT (
    PERSON_ID   INT NOT NULL,
    PROJECT_ID  INT NOT NULL,
    PRIMARY KEY (PERSON_ID, PROJECT_ID)
  );

This intermediary table allows to decompose the m:n association into two 1:n associations. The intermediary table may also hold additional information. For an example the role a certain person plays for a project:

CREATE TABLE PERSON_PROJECT (
    PERSON_ID   INT NOT NULL,
    PROJECT_ID  INT NOT NULL,
    ROLENAME    VARCHAR(20),
    PRIMARY KEY (PERSON_ID, PROJECT_ID)
  );


The decomposition is mandatory on the ER model level. On the OOD level it is not mandatory but may be a valid solution. It is mandatory on the OOD level if the association is qualified (as in our example with a rolename). This will result in the introduction of a association class. A class-diagram reflecting this decomposition looks like:




A Person has a Collection attribute roles containing Role entries. A Project has a Collection attribute roles containing Role entries. A Role has reference attributes to its Person and to its Project.
Handling of 1:n mapping has been explained above. Thus I will finish this section with a short look on the repository entries for the classes test.ojb.broker.Person, test.ojb.broker.Project and test.ojb.broker.Role:

<!-- Definitions for test.ojb.broker.Person -->
   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Person</class.name>
      <table.name>PERSON</table.name>
      <FieldDescriptor id="1">
         <field.name>id</field.name>
         <column.name>ID</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
         <autoincrement>true</autoincrement>
      </FieldDescriptor>
      <FieldDescriptor id="2">
         <field.name>firstname</field.name>
         <column.name>FIRSTNAME</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <FieldDescriptor id="3">
         <field.name>lastname</field.name>
         <column.name>LASTNAME</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <CollectionDescriptor id="1">
         <cdfield.name>roles</cdfield.name>
         <items.class>test.ojb.broker.Role</items.class>
         <inverse_fk_descriptor_ids>1</inverse_fk_descriptor_ids>
      </CollectionDescriptor>
   </ClassDescriptor>

<!-- Definitions for test.ojb.broker.Project -->
   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Project</class.name>
      <table.name>PROJECT</table.name>
      <FieldDescriptor id="1">
         <field.name>id</field.name>
         <column.name>ID</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
         <autoincrement>true</autoincrement>
      </FieldDescriptor>
      <FieldDescriptor id="2">
         <field.name>title</field.name>
         <column.name>TITLE</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <FieldDescriptor id="3">
         <field.name>description</field.name>
         <column.name>DESCRIPTION</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <CollectionDescriptor id="1">
         <cdfield.name>roles</cdfield.name>
         <items.class>test.ojb.broker.Role</items.class>
         <inverse_fk_descriptor_ids>2</inverse_fk_descriptor_ids>
      </CollectionDescriptor>
   </ClassDescriptor>

<!-- Definitions for test.ojb.broker.Role -->
   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Role</class.name>
      <table.name>PERSON_PROJECT</table.name>
      <FieldDescriptor id="1">
         <field.name>person_id</field.name>
         <column.name>PERSON_ID</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
      </FieldDescriptor>
      <FieldDescriptor id="2">
         <field.name>project_id</field.name>
         <column.name>PROJECT_ID</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
      </FieldDescriptor>
      <FieldDescriptor id="3">
         <field.name>roleName</field.name>
         <column.name>ROLENAME</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <ReferenceDescriptor id="1">
         <rdfield.name>person</rdfield.name>
         <referenced.class>test.ojb.broker.Person</referenced.class>
         <fk_descriptor_ids>1</fk_descriptor_ids>
      </ReferenceDescriptor>
      <ReferenceDescriptor id="2">
         <rdfield.name>project</rdfield.name>
         <referenced.class>test.ojb.broker.Project</referenced.class>
         <fk_descriptor_ids>2</fk_descriptor_ids>
      </ReferenceDescriptor>
   </ClassDescriptor>



Support for non-decomposed m:n mappings

If there is no need for a association class on the OOD level (say we are not interested in role information), OJB can be configured to do the m:n mapping transparently. Say a Person is not to have a collection of Role Objects but just to have a Collection of Project objetcs (hold in an attribute projects). Projects also are expected to contain a collection of Persons (hold in attribute persons).

To tell OJB how to handle this m:n association the CollectionDescriptors for the Collection attributes projects and roles need additional information on the intermediary table and the foreign key columns pointing to the PERSON table and the foreign key columns pointing to the PROJECT table. The respective sections are marked bold in the following code from the repository.xml:

<!-- Definitions for test.ojb.broker.Person -->
   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Person</class.name>
      <table.name>PERSON</table.name>
      <FieldDescriptor id="1">
         <field.name>id</field.name>
         <column.name>ID</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
         <autoincrement>true</autoincrement>
      </FieldDescriptor>
      <FieldDescriptor id="2">
         <field.name>firstname</field.name>
         <column.name>FIRSTNAME</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <FieldDescriptor id="3">
         <field.name>lastname</field.name>
         <column.name>LASTNAME</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <CollectionDescriptor id="2">
         <cdfield.name>projects</cdfield.name>
         <items.class>test.ojb.broker.Project</items.class>
         <inverse_fk_descriptor_ids>999</inverse_fk_descriptor_ids>
         <indirection_table>PERSON_PROJECT</indirection_table>
         <fks_pointing_to_this_class>PERSON_ID</fks_pointing_to_this_class>
         <fks_pointing_to_items_class>PROJECT_ID</fks_pointing_to_items_class>
         <auto.retrieve>true</auto.retrieve>
         <auto.update>true</auto.update>
      </CollectionDescriptor>
   </ClassDescriptor>

<!-- Definitions for test.ojb.broker.Project -->
   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Project</class.name>
      <table.name>PROJECT</table.name>
      <FieldDescriptor id="1">
         <field.name>id</field.name>
         <column.name>ID</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
         <autoincrement>true</autoincrement>
      </FieldDescriptor>
      <FieldDescriptor id="2">
         <field.name>title</field.name>
         <column.name>TITLE</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <FieldDescriptor id="3">
         <field.name>description</field.name>
         <column.name>DESCRIPTION</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <CollectionDescriptor id="2">
         <cdfield.name>persons</cdfield.name>
         <items.class>test.ojb.broker.Person</items.class>
         <inverse_fk_descriptor_ids>999</inverse_fk_descriptor_ids>
         <indirection_table>PERSON_PROJECT</indirection_table>
         <fks_pointing_to_this_class>PROJECT_ID</fks_pointing_to_this_class>
         <fks_pointing_to_items_class>PERSON_ID</fks_pointing_to_items_class>
         <auto.retrieve>true</auto.retrieve>
         <auto.update>true</auto.update>
      </CollectionDescriptor>
   </ClassDescriptor>


That's all that needs to be configured! See the code in class test.ojb.broker.MtoNMapping for JUnit testmethods using the classes Person, Project and Role.


setting load-, update- and delete-cascading

As shown in the sections on 1:1 and 1:n mappings OJB manages associations (or object references in Java terminology) by declaring special Reference- and CollectionDescriptors. These Descriptor may contain some additional information that modifies OJB's behaviour on object materialization, -updating and -deletion. If nothing is specified default values are assumed:

  1. On materializing an Object from the RDBMS with PersistenceBroker.getObjectByQuery(...)all it's referenced objects (both 1:1 and 1:n associations) are materialized as well. This results in completely materialized object nets.

  2. On updating or inserting an object with PersistenceBroker.store(...) referenced objects are NOT updated by default.

  3. On deleting an object with PersistenceBroker.delete(...) referenced objects are NOT deleted by default.

These default settings are explicitly set in the following Reference- and CollectionDescriptor. Of course they can be changed to implement special semantics.

      <ReferenceDescriptor id="1">
         <rdfield.name>productGroup</rdfield.name>
         <referenced.class>test.ojb.broker.ProductGroup</referenced.class>
         <fk_descriptor_ids>4</fk_descriptor_ids>
         <auto.retrieve>true</auto.retrieve>
         <auto.update>false</auto.update>
         <auto.delete>false</auto.delete>
      </ReferenceDescriptor>

      <CollectionDescriptor id="1">
         <cdfield.name>allArticlesInGroup</cdfield.name>
         <items.class>test.ojb.broker.Article</items.class>
         <inverse_fk_descriptor_ids>4</inverse_fk_descriptor_ids>
         <auto.retrieve>true</auto.retrieve>
         <auto.update>false</auto.update>
         <auto.delete>false</auto.delete>
      </CollectionDescriptor>

These default settings are required for proper operation of the ODMG implementation. If you plan to use the ODMG implementation you should just ommit these tags.


using proxy classes

Proxy classes can be used for "lazy loading" aka "lazy materialization". Using Proxy classes can help you in reducing unneccessary db lookups. As an example we take a ProductGroup object pg which contains a collection of 15 Article objects. Now we examine what happens when pg is loaded from the database:

Without using proxies all 15 associated Article objects are immediately loaded from the db, even if you are not interested in them but just want to lookup the description-attribute of the ProductGroup object pg.

If proxies are used, the collection is filled with 15 proxy objects, that implement the same interface as the "real objects" but contain only an OID and a void reference. The 15 article objects are not instantiated. Only when a method is invoked on such a proxy object it loads its "real subject" by OID and delegates the method call to it. Using this dynamic delegation mechanism instantiation of persistent objects and database lookups can be minimized.

To use proxies the persistent class in question (in our case the Article class) must implement an interface (for example InterfaceArticle). This interface is needed to allow replacement of the proper Article object with a proxy implementing the same interface. Have a look at the code:

public class Article implements InterfaceArticle
{
    /** maps to db-column "Artikel-Nr"; PrimaryKey*/
    protected int articleId;
    /** maps to db-column "Artikelname"*/
    protected String articleName;

    ...

    public int getArticleId()
    {
        return articleId;
    }

    public java.lang.String getArticleName()
    {
        return articleName;
    }

    ...
}

public interface InterfaceArticle
{
    public int getArticleId();

    public java.lang.String getArticleName();

    ...
}



public class ArticleProxy extends VirtualProxy implements InterfaceArticle
{
    public ArticleProxy(ojb.broker.Identity uniqueId, PersistenceBroker broker)
    {
        super(uniqueId, broker);
    }

    public int getArticleId()
    {
        return realSubject().getArticleId();
    }

    public java.lang.String getArticleName()
    {
        return realSubject().getArticleName();
    }

    private InterfaceArticle realSubject()
    {
        try
        {
            return (InterfaceArticle) getRealSubject();
        }
        catch (Exception e)
        {
            return null;
        }
    }
}

The proxy is constructed from the identity of the real subject. All Method calls are delegated to the object returned by realSubject().
This method uses getRealSubject() from the base class VirtualProxy:

    public Object getRealSubject() throws PersistenceBrokerException
    {
        return indirectionHandler.getRealSubject();
    }

The proxy delegates the the materialization work to its indirectionHandler (ojb.broker.accesslayer.IndirectionHandler). If the real subject is not yet materialized, a PersistenceBroker is used to retrieve it by its OID:

    public synchronized Object getRealSubject() throws PersistenceBrokerException
    {
        if (realSubject == null)
        {
            materializeSubject();
        }
        return realSubject;
    }

    private void materializeSubject() throws PersistenceBrokerException
    {
        realSubject = broker.getObjectByIdentity(id);
    }



To tell OJB to use proxy objects instead of materializing full Article objects we have to add the following section to the XML repository file:

   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Article</class.name>
      <class.proxy>test.ojb.broker.ArticleProxy</class.proxy>
      ...

The following class diagram shows the relationships between all above mentioned classes:






Using dynamic proxies

The implementation of a proxy class is a boring task that repeats the same delegation scheme for each new class. To liberate the developer from this unproductive job OJB provides a dynamic proxy solution based on the JDK 1.3 dynamic proxy concept. (For JDK1.2 we ship a replacement for the required java.lang.reflect classes. Credits for this solution to ObjectMentor.) If you are interested in the mechanics have a look at the class ojb.broker.accesslayer.IndirectionHandler, that does all the hard work.

To use a dynamic proxy for lazy materialization of Article objects we have to declare it in the repository.xml file.

   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Article</class.name>
      <class.proxy>dynamic</class.proxy>
      ...

To use dynamic proxies the persistent class in question (in our case the Article class) must implement an interface (for example InterfaceArticle). This interface is needed to allow replacement of the proper Article object with a dynamic proxy implementing the same interface.

Using a single Proxy for a whole Collection

A collection proxy represents a whole collection of objects, where as a proxy class represents a single object.
The advantage of this concept is a reduced number of db-calls compared to using proxy classes. A collection proxy only needs a single db-call to materialize all it's objects. This happens the first time its content is accessed (ie: by calling iterator();). An additional db-call is used to calculate the size of the collection. So collection proxy is mainly used as a deferred execution of a query. Have a look at class ojb.broker.accesslayer.CollectionProxy for further details.

The following mapping shows how to use a collection proxy for a relationship:

<!-- Definitions for test.ojb.broker.ProductGroup -->
   <ClassDescriptor id="2">
      <class.name>test.ojb.broker.ProductGroup</class.name>
      <table.name>Kategorien</table.name>
      <FieldDescriptor id="1">
         <field.name>groupId</field.name>
         <column.name>Kategorie_Nr</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
         <autoincrement>true</autoincrement>
      </FieldDescriptor>
      ...
     <CollectionDescriptor id="1">
         <proxyCollection>true</proxyCollection>
         <cdfield.name>allArticlesInGroup</cdfield.name>
         <items.class>test.ojb.broker.Article</items.class>
         <inverse_fk_descriptor_ids>4</inverse_fk_descriptor_ids>
         </CollectionDescriptor>
   </ClassDescriptor>
The classes participating in this relationship do not need to implement a special interface to be used in a collection proxy.

Although it's possible to mix the collection proxy concept with the proxy class concept, it's not recommended because it increases the number of db-calls.

Using a Proxy for a Reference

A proxy reference is based on the original proxy class concept. The main difference is that the ReferenceDescriptor defines when to use a proxy class and not the ClassDescriptor.
In the following mapping the class ProductGroup is not defined to be a proxy class in it's ClassDescriptor. Only for shown relationship a proxy of ProductGroup should be used:

   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Article</class.name>
      <table.name>Artikel</table.name>
      ...
         <ReferenceDescriptor id="1">
            <proxyReference>true</proxyReference>
            <rdfield.name>productGroup</rdfield.name>
            <referenced.class>test.ojb.broker.ProductGroup</referenced.class>
            <fk_descriptor_ids>4</fk_descriptor_ids>
         </ReferenceDescriptor>
   </ClassDescriptor>
Because a proxy reference is only about the location of the definition, the referenced class must implement a special interface (see using proxy classes).


type and value conversions

Say your database column contains INTEGER values but you have to use boolean attributes in your Domain objects. You need a type- and value mapping described by a ConversionStrategy! Follow this link to learn more.


extents and polymorphism

Working with inheritance hierarchies is a common task in object oriented design and programming. Of course any serious Java O/R tool must support ineritance and interfaces for persistent classes. To see how OJB does this job, I will again demonstrate some sample persistent classes from the package test.ojb.broker.

There is a primary interface "InterfaceArticle". This interface is implemented by "Article" and "CdArticle". There is also a class "BookArticle" derived from "Article". (See the following class diagram for details)





polymorphism

OJB allows to use interfaces or (possibly abstract) baseclasses in queries or in type definitions of reference attributes. A Query against the interface InterfaceArticle must not only return objects of type Article but also of CdArticle and BookArticle! The following test method searches for all objects implementing InterfaceArticle with an articleName equal to "Hamlet". The Collections is filled with one matching BookArticle object.

    public void testCollectionByQuery()
    {
        try
        {
            Criteria crit = new Criteria();
            crit.addEqualTo("articleName", "Hamlet");
            Query q = QueryFactory.newQuery(InterfaceArticle.class, crit);

            Collection result = broker.getCollectionByQuery(q);

            System.out.println(result);

            assertNotNull("should return at least one item", result);
            assertTrue("should return at least one item", result.size() > 0);

        }
        catch (Throwable t)
        {
            fail(t.getMessage());
        }
    }



Of course it is also possible to define reference attributes of an interface or baseclass type. In all above examples Article has a reference attribute of type InterfaceProductGroup.


extents

The query in the last example just returned one object. Now imagine a query against InterfaceArticle with no selecting criteria. What do you expect to happen?
Right: OJB returns ALL objects implementing InterfaceArticle. I.e. All Articles, BookArticles and CdArticles. The following method prints out the collection of all InterfaceArticle objects:

    public void testExtentByQuery()
    {
        try
        {
            // no criteria signals to omit a WHERE clause
            Query q = QueryFactory.newQuery(InterfaceArticle.class, null);

            Collection result = broker.getCollectionByQuery(q);


            System.out.println("OJB proudly presents: The InterfaceArticle Extent\n" + result);

            assertNotNull("should return at least one item", result);
            assertTrue("should return at least one item", result.size() > 0);

        }
        catch (Throwable t)
        {
            fail(t.getMessage());
        }
    }

The set of all instances of a class (whether living in memory or stored in a persistent medium) is called Extent in ODMG and JDO terminology. OJB extends this notion slightly, as all objects implementing a given interface are regarded as members of the interfaces extent.

In our class diagram we find:

  1. two simple "one-class-only" extents, BookArticle and CdArticle.

  2. A compound extent Article containing all Article and BookArticle instances.

  3. An interface extent containing all Article, BookArticle and CdArticle instances.

There is no extra coding necessary to define extents, but they have to be declared in the repository file. The classes from the above example require the following declarations:

  1. "one-class-only" extents require no declaration

  2. A declaration for the baseclass Article, defining which classes are subclasses of Article:

<!-- Definitions for test.ojb.broker.Article -->
   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Article</class.name>
      <class.extent>test.ojb.broker.BookArticle</class.extent>
      ...
   </ClassDescriptor>
 
  1. A declaration for InterfaceArticle, defining which classes implement this interface:

<!-- Definitions for test.ojb.broker.InterfaceArticle -->
   <ClassDescriptor id="10">
      <ExtentDescriptor>
         <class.name>test.ojb.broker.InterfaceArticle</class.name>
         <class.extent>test.ojb.broker.Article</class.extent>
         <class.extent>test.ojb.broker.BookArticle</class.extent>
         <class.extent>test.ojb.broker.CdArticle</class.extent>
      </ExtentDescriptor>
   </ClassDescriptor>

Why is it necessary to explicitely declare which classes implement an interface and which classes are derived from a baseclass? Of course it is quite simple in Java to check whether a class implements a given interface or extends some other class. But sometimes it may not be appropiate to treat special implementors (e.g. proxies) as proper implementors.
Other problems might arise because a class may implement multiple interfaces, but is only allowed to be regarded as member of one extent.
In other cases it may be neccessary to treat certain classes as implementors of an interface or as derived from a base even if they are not.
As an example you will find that the ClassDescriptor for class test.ojb.broker.Article in the repository.xml contains an entry declaring class CdArticle as a derived class:

<!-- Definitions for test.ojb.broker.Article -->
   <ClassDescriptor id="1">
      <class.name>test.ojb.broker.Article</class.name>
      <class.extent>test.ojb.broker.BookArticle</class.extent>
      <class.extent>test.ojb.broker.CdArticle</class.extent>
      ...
   </ClassDescriptor>

mapping inheritance hierarchies

In the literature on object/relational mapping the problem of mapping oo-inheritance hierarchies to RDBMS has been widely covered. I'll give only a rough overview. Have a look at the following inheritance hierarchy:




If we have to define database tables that have to contain these classes we have to choose one of the following solutions:

  1. Map all classes onto one table. A DDL for the table would look like:

CREATE TABLE A_EXTENT
(
    ID                 INT NOT NULL PRIMARY KEY,
    SOME_VALUE_FROM_A  INT,
    SOME_VALUE_FROM_B  INT
)
  1. Map each class to a distinct table and have all attributes from the base class in the derived class. DDL for the table could look like:

CREATE TABLE A
(
    ID                 INT NOT NULL PRIMARY KEY,
    SOME_VALUE_FROM_A  INT
)
CREATE TABLE B
(
    ID                 INT NOT NULL PRIMARY KEY,
    SOME_VALUE_FROM_A  INT,
    SOME_VALUE_FROM_B  INT
)
  1. Map each class to a distinct table, but don't map base class fields to derived classes. Use joins to materialize over all tables to materialize objects. DDL for the table would look like:

CREATE TABLE A
(
    ID                 INT NOT NULL PRIMARY KEY,
    SOME_VALUE_FROM_A  INT
)
CREATE TABLE B
(
    A_ID               INT NOT NULL,
    SOME_VALUE_FROM_B  INT
)



Currently OJB does only provide direct support for approaches 1.) and 2.). In the following I show how these mapping approaches can be implemented by using OJB.

mapping all classes on the same table

Mapping several classes on one table works well underOJB. There is only one special situation that needs some attention:

Say there is a baseclass AB with derived classes A and B. A and B are mapped on a table AB_TABLE. Storing A and B objects to this table works fine. But now consider a Query against the baseclass AB. How can the correct type of the stored objects be determined?
OJB needs a column of type CHAR or VARCHAR that contains the classname to be used for instantiation. This column must be mapped on a special attribute ojbConcreteClass. On loading objects from the table OJB checks this attribute and instantiates objects of this type.
There is sample code for this feature in the method test.ojb.broker.PersistenceBrokerTest.testMappingToOneTable().

See the mapping details in the following Class declaration and the respective mapping:

public abstract class AB
{
    /** the special attribute telling OJB the object's concrete type.
     *  NOTE: this attribute MUST be called ojbConcreteClass
     */
    protected String ojbConcreteClass;

}

public class A extends AB
{
    int id;
    int someValue;

    public A()
    {
        // OJB must know the type of this object
        ojbConcreteClass = A.class.getName();
    }
}

<!-- Definitions for test.ojb.broker.AB -->
   <ClassDescriptor id="AB">
      <ExtentDescriptor>
         <class.name>test.ojb.broker.AB</class.name>
         <class.extent>test.ojb.broker.A</class.extent>
         <class.extent>test.ojb.broker.B</class.extent>
      </ExtentDescriptor>
   </ClassDescriptor>

<!-- Definitions for test.ojb.broker.A -->
   <ClassDescriptor id="A">
      <class.name>test.ojb.broker.A</class.name>
      <table.name>AB_TABLE</table.name>
      <FieldDescriptor id="1">
         <field.name>id</field.name>
         <column.name>ID</column.name>
         <jdbc_type>INTEGER</jdbc_type>
         <PrimaryKey>true</PrimaryKey>
         <autoincrement>true</autoincrement>
      </FieldDescriptor>
      <FieldDescriptor id="2">
         <field.name>ojbConcreteClass</field.name>
         <column.name>CLASS_NAME</column.name>
         <jdbc_type>VARCHAR</jdbc_type>
      </FieldDescriptor>
      <FieldDescriptor id="3">
         <field.name>someValue</field.name>
         <column.name>VALUE</column.name>
         <jdbc_type>INTEGER</jdbc_type>
      </FieldDescriptor>
   </ClassDescriptor>

The column CLASS_NAME is used to store the concrete type of each object. If you can't provide such an additional column, but have to use some other way of indicating the type of each object you need some additional programming:

You have to derive a Class from ojb.broker.accesslayer.RowReaderDefaultImpl and overwrite the method selectClassDescriptor to implement your specific type selection mechanism. The code of the default implementation looks like follows:

protected ClassDescriptor selectClassDescriptor(ResultSet rs, ClassDescriptor cld)
{
    // check if there is an attribute which tells us which concrete class is to be instantiated
    FieldDescriptor fld = cld.getFieldDescriptorByName("ojbConcreteClass");
    if (fld == null)
        return cld;
    else
    {
       try
       {
           // select the ClassDescriptor for the class specified in the ojbConcreteClass attribute
           String concreteClass = rs.getString(fld.getColumnName());
           ClassDescriptor result =
               DescriptorRepository.getInstance().getDescriptorFor(Class.forName(concreteClass));
           if (result == null) result = cld;
           return result;
       }
       catch (Exception e)
       {
           return cld;
       }
    }
}

After implementing this Class you must edit the ClassDescriptor of the respective Class in the XML repository to specify the usage of your RowReader Implementation:

<rowReader>my.own.RowReaderImpl</rowReader>

You will learn more about RowReaders in the next section.

mapping each class on a distinct table

This is the most simple solution. Just write a complete ClassDescriptor for each class that contains FieldDescriptors for all (also inherited) attributes.

mapping classes on multiple joined tables

This approach is not directly covered. If you have to use this approach you have to map it explicitely as a 1:1 association between a B object and an A object to retrieve all attributes from the table for A.


using rowreaders

RowReaders provide a Callback mechanism that allows to interact with the OJB load mechanism. To understand how to use them we must know some of the details of the load mechanism.

To materialize objects from a rdbms OJB uses RsIterators, that are essentially wrappers to JDBC ResultSets. RsIterators are constructed from queries against the Database.

The RsIterator.next() is used to materialize the next object from the underlying ResultSet. This method first checks if the underlying ResultSet is not yet exhausted and then delegates the construction of an Object from the current ResultSet row to the method getObjectFromResultSet():

    private Object getObjectFromResultSet()
    {
        try
        {
            // 1.materialize Object with primitive attributes filled from current row
            // (m_mif holds the ClassDescriptor containing metadata on the target Class)
            Object result = m_mif.getRowReader().readObjectFrom(m_rs, m_mif);

            // 2. check if Object is in cache. if so return cached version.
            //    If Object is not in cache fill reference and collection attributes
            Identity oid = new Identity(result);
            Object check = cache.lookup(oid);
            if (check != null)
                return check;
            else
            {
                // cache object immediately
                cache.cache(oid, result);
                // retrieve reference and collection attributes
                m_broker.retrieveReferences(result, m_mif);
                m_broker.retrieveCollections(result, m_mif);
                return result;
            }
        }
        catch (Exception ex)
        {
            System.out.println(ex.getMessage());
            ex.printStackTrace();
        }
        return null;
    }

This method first uses the RowReader used for the respective Class to instantiate a new object and fill its primitive attributes from the current ResultSet row.
In the second step OJB checks if there is already a cached version of this object. If so the cache version is returned. If not, the object is fully materialized by filling reference- and collection-attributes and then returned.
The RowReader to be used for a Class can be configured in the XML repository with the tag <rowReader>. If no RowReader is specified, the RowReaderDefaultImpl is used. The method readObjectFrom(...) of this class looks like follows:

    public Object readObjectFrom(ResultSet rs, ClassDescriptor descriptor)
    {
        Object val = null;
        FieldDescriptor fmd = null;
        // selectClassDescriptor may be used to select a ClassDescriptor for a derived
        // concrete class. See example in the section 'mapping all classes to the same table'.
        ClassDescriptor cld = selectClassDescriptor(rs, descriptor);
        Constructor multiArgsConstructor = cld.getConstructor();
        ConversionStrategy conversion = cld.getConversionStrategy();
        Object result = null;
        try
        {
            // if the class has an multiargument constructor, use it to construct the new object
            if (multiArgsConstructor != null)
            {
                result = buildWithMultiArgsConstructor(cld, rs, conversion, multiArgsConstructor);
            }
            // if no such constructor exists, use default constructor and reflection to fill object
            else
            {
                result = buildWithReflection(cld, rs, conversion);

            }
            return result;

        }
        catch (Exception ex)
        {
            System.out.println(ex.getMessage());
            ex.printStackTrace();
            return null;
        }
    }

The method proceeds in two steps:
1. it selects the ClassDescriptor two be used for the materialization of the object. This may be useful in mapping multiple classes on the same table.
2. It checks if there is a MultiArgument-comstructor that can be filled with all persistent attributes of the class or not. If there is such a constructor it uses it for instantiation. If there is only a public default constructor, OJB must use it an has to fill all attributes by means of Java reflection.

By writing your own RowReader you can provide additional features to the loading mechanism.

rowreader example

Assume that for some reason we don't want to map a 1:1 association with a foreign key relationship to a different database table but read the associated object 'inline' from some columns of the master object's table.

The class test.ojb.broker.ArticleWithStockDetail has a stockDetail attribute, holding a reference to a StockDetail object. The class StockDetail is not declared in the XML repository. Thus OJB is not able to fill this attribute by ordinary mapping techniques.

We have to define a RowReader that does the proper initialization. The Class test.ojb.broker.RowReaderTestImpl extends the RowReaderDefaultImpl and overrides the readObjectFrom(...) method as follows:

    public Object readObjectFrom(ResultSet rs, ClassDescriptor cld)
    {
        // 1. read object with the normal RowReaderDefaultImpl semantics
        Object result = super.readObjectFrom(rs, cld);
        // 2. if object is an ArticleWithStockDetail fill the stockDetail attribute:
        if (result instanceof ArticleWithStockDetail)
        {
            ArticleWithStockDetail art = (ArticleWithStockDetail) result;
            // read stockDetail attributes
            boolean sellout = art.isSelloutArticle;
            int minimum = art.minimumStock;
            int ordered = art.orderedUnits;
            int stock = art.stock;
            String unit = art.unit;
            // create StockDetail
            StockDetail detail = new StockDetail(sellout, minimum, ordered, stock, unit, art);
            // set the reference attribute
            art.stockDetail = detail;
            return art;
        }

        return result;
    }

To activate this RowReader the ClassDescriptor for the class ArticleWithStockDetail contains the following entry:

<rowReader>test.ojb.broker.RowReaderTestImpl</rowReader>

$FOOTER$