The Apache DB Project
ObJectRelationalBridge

OJB

Downloads

Documentation

Development

Translated (Web)

Advanced Object Relational Mapping techniques

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 org.apache.ojb.broker. This is working code from the JUnit Testsuite. Thus it is guaranteed to work. It should be quite straightforward to reuse these samples to build up your own applications. I hope you will find this hands on approach helpful for building your own OJB based applications.

table of contents
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 reference-descriptor to the class-descriptor of the Article class. This descriptor contains the following information:

  1. The attribute implementing the association (name="productGroup") is productGroup.
  2. The referenced object is of type (class-ref="org.apache.ojb.broker.ProductGroup") org.apache.ojb.broker.ProductGroup.
  3. A reference-descriptor contains one or more foreignkey elements. These elements define foreign key attributes. The element contains the name of the field-descriptor describing the foreignkey fields. The FieldDescriptor with the name "productGroupId" describes the foreignkey attribute productGroupId:
    <field-descriptor
        name="productGroupId"
        column="Kategorie_Nr"
        jdbc-type="INTEGER"
    />
    		

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

<!-- Definitions for org.apache.ojb.ojb.broker.Article -->
   <class-descriptor
      class="org.apache.ojb.broker.Article"
      proxy="dynamic"
      table="Artikel"
   >
      <extent-class class-ref="org.apache.ojb.broker.BookArticle" />
      <extent-class class-ref="org.apache.ojb.broker.CdArticle" />
      <field-descriptor
         name="articleId"
         column="Artikel_Nr"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="articleName"
         column="Artikelname"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="supplierId"
         column="Lieferanten_Nr"
         jdbc-type="INTEGER"
      />
      <field-descriptor
         name="productGroupId"
         column="Kategorie_Nr"
         jdbc-type="INTEGER"
      />
        ...
      <reference-descriptor
         name="productGroup"
         class-ref="org.apache.ojb.broker.ProductGroup"
      >
         <foreignkey field-ref="productGroupId"/>
      </reference-descriptor>
   </class-descriptor>

  

This example provides unidirectional navigation only. Bidirectional navigation may be added by including a reference from a ProductGroup to a single Article (for example, a sample article for the productgroup). To accomplish this we need to perform the following steps:

  1. Add a private Article attribute named sampleArticle to the class ProductGroup.
  2. Add a private int attribute named sampleArticleId to the ProductGroup class representing the foreign key.
  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:
<field-descriptor
    name="sampleArticleId"
    column="SAMPLE_ARTICLE_ID"
    jdbc-type="INTEGER"
/>
  
  1. Add a ReferenceDescriptor to the ClassDescriptor of the Class ProductGroup:
<reference-descriptor
    name="sampleArticle"
    class-ref="org.apache.ojb.broker.Article"
>
    <foreignkey field-ref="sampleArticleId""/>
</reference-descriptor>
  
Mapping 1:n associations

We will take a different perspective from the previous exmaple for a 1:n association. We will associate multiple Articles with a single ProductGroup. This association is navigable only from the ProductGroup to its Article instances. 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 on the ProductGroup class. As in the previous example, the Article class contains a foreignkey attribute named productGroupId that identifies an Article's ProductGroup. The Database table are the same as above.

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

  1. The attribute implementing the association (name="allArticlesInGroup")
  2. The class of the elements in the collection (element-class-ref="org.apache.ojb.broker.Article")
  3. The name of field-descriptor of the element class used as foreign key attributes are defined in inverse-foreignkey elements: ( this is again pointing to the field-descriptor for the attribute productGoupId in class Article).
  4. optional attributes to define the sort order of the retrieved collection: orderby="articleId" sort="DESC".

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

<!-- Definitions for org.apache.ojb.broker.ProductGroup -->
   <class-descriptor
      class="org.apache.ojb.broker.ProductGroup"
      table="Kategorien"
   >
      <field-descriptor
         name="groupId"
         column="Kategorie_Nr"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="groupName"
         column="KategorieName"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="description"
         column="Beschreibung"
         jdbc-type="VARCHAR"
      />
      <collection-descriptor
         name="allArticlesInGroup"
         element-class-ref="org.apache.ojb.broker.Article"
         orderby="articleId"
         sort="DESC"
      >
         <inverse-foreignkey field-ref="productGroupId"/>
      </collection-descriptor>
   </class-descriptor>
  


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 Collection 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 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 object model level it is not mandatory, but may be a valid solution. It is mandatory on the object 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 we will finish this section with a short look at the repository entries for the classes org.apache.ojb.broker.Person, org.apache.ojb.broker.Project and org.apache.ojb.broker.Role:

<!-- Definitions for org.apache.ojb.broker.Person -->
   <class-descriptor
      class="org.apache.ojb.broker.Person"
      table="PERSON"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="firstname"
         column="FIRSTNAME"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="lastname"
         column="LASTNAME"
         jdbc-type="VARCHAR"
      />
      <collection-descriptor
         name="roles"
         element-class-ref="org.apache.ojb.broker.Role"
      >
         <inverse-foreignkey field-ref="person_id"/>
      </collection-descriptor>
      ...
   </class-descriptor>

<!-- Definitions for org.apache.ojb.broker.Project -->
   <class-descriptor
      class="org.apache.ojb.broker.Project"
      table="PROJECT"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="title"
         column="TITLE"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="description"
         column="DESCRIPTION"
         jdbc-type="VARCHAR"
      />
      <collection-descriptor
         name="roles"
         element-class-ref="org.apache.ojb.broker.Role"
      >
         <inverse-foreignkey field-ref="project_id"/>
      </collection-descriptor>
      ...
   </class-descriptor>

<!-- Definitions for org.apache.ojb.broker.Role -->
   <class-descriptor
      class="org.apache.ojb.broker.Role"
      table="PERSON_PROJECT"
   >
      <field-descriptor
         name="person_id"
         column="PERSON_ID"
         jdbc-type="INTEGER"
         primarykey="true"
      />
      <field-descriptor
         name="project_id"
         column="PROJECT_ID"
         jdbc-type="INTEGER"
         primarykey="true"
      />
      <field-descriptor
         name="roleName"
         column="ROLENAME"
         jdbc-type="VARCHAR"
      />
      <reference-descriptor
         name="person"
         class-ref="org.apache.ojb.broker.Person"
      >
         <foreignkey field-ref="person_id"/>
      </reference-descriptor>
      <reference-descriptor
         name="project"
         class-ref="org.apache.ojb.broker.Project"
      >
         <foreignkey field-ref="project_id"/>
      </reference-descriptor>
   </class-descriptor>



Support for Non-Decomposed m:n Mappings

If there is no need for an association class at the object level (we are not interested in role information), OJB can be configured to do the m:n mapping transparently. For example, a Person does not have a collection of Role objects but only a Collection of Project objects (held in the attribute projects). Projects also are expected to contain a collection of Person objects (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:

   <class-descriptor
      class="org.apache.ojb.broker.Person"
      table="PERSON"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="firstname"
         column="FIRSTNAME"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="lastname"
         column="LASTNAME"
         jdbc-type="VARCHAR"
      />
      ...
      <collection-descriptor
         name="projects"
         element-class-ref="org.apache.ojb.broker.Project"
         auto-retrieve="true"
         auto-update="true"
         indirection-table="PERSON_PROJECT"
      >
         <fk-pointing-to-this-class column="PERSON_ID"/>
         <fk-pointing-to-element-class column="PROJECT_ID"/>
      </collection-descriptor>
   </class-descriptor>

<!-- Definitions for org.apache.ojb.broker.Project -->
   <class-descriptor
      class="org.apache.ojb.broker.Project"
      table="PROJECT"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="title"
         column="TITLE"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="description"
         column="DESCRIPTION"
         jdbc-type="VARCHAR"
      />
      ...
      <collection-descriptor
         name="persons"
         element-class-ref="org.apache.ojb.broker.Person"
         auto-retrieve="true"
         auto-update="false"
         indirection-table="PERSON_PROJECT"
      >
         <fk-pointing-to-this-class column="PROJECT_ID"/>
         <fk-pointing-to-element-class column="PERSON_ID"/>
      </collection-descriptor>
   </class-descriptor>

That is all that needs to be configured! See the code in class org.apache.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 Collection Descriptors. 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. auto-retrieve="true"
    By default materializing an Object from the RDBMS with PersistenceBroker.getObjectByQuery(...) cause all its referenced objects (both 1:1 and 1:n associations) to be materialized as well. (If OJB is configured to use proxies, the referenced objects are not materialized immmediately, but lazy loading proxy objects are used instead.)
  2. auto-update="false"
    On updating or inserting an object with PersistenceBroker.store(...) referenced objects are NOT updated by default.
  3. auto-delete="false"
    On deleting an object with PersistenceBroker.delete(...) referenced objects are NOT deleted by default.

If no special settings are made in the reference-descriptor or collection-descriptor elements, these default settings are used.

These default settings are mandatory for proper operation of the ODMG and JDO implementation.

The default settings can be changed as follows:

  1. auto-retrieve="false"
    With this setting references and and Collection attributes will not be loaded by OJB on loading instances. OJB will also not replace those attributes with dynamic proxies. These attributes simply stay set to null. If such attributes must be accessed later, the user is responsible to load them manually with the PersistenceBroker.retrieveReference(...) PersistenceBroker.retrieveAllReferences(...) methods.
  2. auto-update="true"
    If this flag is set on a reference- or collection-attribute the referenced objects are also persisted (inserted or updated) to the database. Objects that have been removed from collections are not automatically handled by the PersistenceBroker. By using special collections like the RemovalAwareCollection you can achieve automatic handling of deletion from collections.
  3. auto-delete="true"
    If this flag is set on a reference- or collection-attribute the referenced objects are also deleted if an instance of the persistent class is deleted. The PersistenceBroker does not provide a full cascading delete, but does only remove objects that are actually referenced. Assume you load a master object that has a collection of 5 detail items and remove 2 of them. If the master object is deleted only the three remaining detail items are deleted. As the other two detail items are not deleted, this could result in foreign key violations if handled carelessly.

In the following code sample, a reference-descriptor and a collection-descriptor are configured to use cascading retrieval (auto-retrieve="true"), insert, and update (auto-update="true") and delete (auto-delete="true") operations:

      <reference-descriptor
         name="productGroup"
         class-ref="org.apache.ojb.broker.ProductGroup"
         auto-retrieve="true"
         auto-update="true"
         auto-delete="true"
      >
         <foreignkey field-ref="productGroupId"/>
      </reference-descriptor>

      <collection-descriptor
         name="allArticlesInGroup"
         element-class-ref="org.apache.ojb.broker.Article"
         auto-retrieve="true"
         auto-update="true"
         auto-delete="true"
         orderby="articleId"
         sort="DESC"
      >
         <inverse-foreignkey field-ref="productGroupId"/>
      </collection-descriptor>
Using Proxy Classes

Proxy classes can be used for "lazy loading" aka "lazy materialization". Using Proxy classes can help you in reducing unneccessary database lookups. There are two kind of proxy mechanisms available:

  1. Dynamic proxies provided by OJB. They can simply be activated by setting certain switches in repository.xml. This is the solution recommemded for most cases.
  2. User defined proxies. User defined proxies allow the user to write proxy implementations.
As it is important to understand the mechanics of the proxy mechanism I highly recommend to read this section before turning to the next sections "using dynamic proxies", "using a single proxy for a whole collection" and "using a proxy for a reference", covering dynamic proxies.

As a simple example we take a ProductGroup object pg which contains a collection of fifteen Article objects. Now we examine what happens when the ProductGroup is loaded from the database:

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

If proxies are used, the collection is filled with fifteen proxy objects, that implement the same interface as the "real objects" but contain only an OID and a void reference. The fifteen article objects are not instantiated when the ProductGroup is initially materialized. Only when a method is invoked on such a proxy object will it load its "real subject" and delegate 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 (org.apache.ojb.broker.accesslayer.IndirectionHandler). If the real subject has not yet been 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:

<class-descriptor
    class="org.apache.ojb.broker.Article"
    proxy="org.apache.ojb.broker.ArticleProxy"
    table="Artikel"
>
...

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 org.apache.ojb.broker.accesslayer.IndirectionHandler, that does all of the hard work.

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

<class-descriptor
  class="org.apache.ojb.broker.Article"
  proxy="dynamic"
  table="Artikel"
>
...

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 if size() is called before loading the data. 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
org.apache.ojb.broker.ProductGroupWithCollectionProxy -->
<class-descriptor
  class="org.apache.ojb.broker.ProductGroupWithCollectionProxy"
  table="Kategorien"
>
  <field-descriptor
     name="groupId"
     column="Kategorie_Nr"
     jdbc-type="INTEGER"
     primarykey="true"
  />
  ...
  <collection-descriptor
     name="allArticlesInGroup"
     element-class-ref="org.apache.ojb.broker.Article"
     proxy="true"
  >
     <inverse-foreignkey field-ref="productGroupId"/>
  </collection-descriptor>
</class-descriptor>

The classes participating in this relationship do not need to implement a special interface to be used in a collection proxy.

Although it is possible to mix the collection proxy concept with the proxy class concept, it is not recommended because it increases the number of database calls.

Customizing a Collection Proxy Class

The optional CollectionProxy entry in OJB.properties defines the class to be used for Collection Proxies. If this entry is null org.apache.ojb.broker.accesslayer.ListProxy is used for Lists and org.apache.ojb.broker.accesslayer.CollectionProxy for Collections.

...
#-----------------------------------------------
# CollectionProxy class
#-----------------------------------------------
#
#CollectionProxyClass=
#
...
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 its ClassDescriptor. Only for shown relationship a proxy of ProductGroup should be used:

<!-- Definitions for org.apache.ojb.broker.ArticleWithReferenceProxy -->
   <class-descriptor
      class="org.apache.ojb.broker.ArticleWithReferenceProxy"
      table="Artikel"
   >
      <field-descriptor
         name="articleId"
         column="Artikel_Nr"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      ...
      <reference-descriptor
         name="productGroup"
         class-ref="org.apache.ojb.broker.ProductGroup"
     proxy="true"
      >
         <foreignkey field-ref="productGroupId"/>
      </reference-descriptor>
   </class-descriptor>

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 FieldConversion!

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 inheritance and interfaces for persistent classes. To demonstrate we will look at some of the JUnit TestSuite classes.

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 us to use interfaces, abstract, or concrete base classes 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 Collection is filled with one matching BookArticle object.

public void testCollectionByQuery() throws Exception
{
    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);
}

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 returned just one object. Now, imagine a query against the InterfaceArticle interface with no selecting criteria. OJB returns all the objects implementing InterfaceArticle. I.e. All Articles, BookArticles and CdArticles. The following method prints out the collection of all InterfaceArticle objects:

public void testExtentByQuery() throws Exception
{
    // 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);
}

The set of all instances of a class (whether living in memory or stored in a persistent medium) is called an Extent in ODMG and JDO terminology. OJB extends this notion slightly, as all objects implementing a given interface are regarded as members of the interface's 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 org.apache.ojb.ojb.broker.Article -->
   <class-descriptor
      class="org.apache.ojb.broker.Article"
      proxy="dynamic"
      table="Artikel"
   >
      <extent-class class-ref="org.apache.ojb.broker.BookArticle" />
      <extent-class class-ref="org.apache.ojb.broker.CdArticle" />
   ...
   </class-descriptor>
  1. A declaration for InterfaceArticle, defining which classes implement this interface:
<!-- Definitions for org.apache.ojb.broker.InterfaceArticle -->
   <class-descriptor class="org.apache.ojb.broker.InterfaceArticle">
      <extent-class class-ref="org.apache.ojb.broker.Article" />
      <extent-class class-ref="org.apache.ojb.broker.BookArticle" />
      <extent-class class-ref="org.apache.ojb.broker.CdArticle" />
   </class-descriptor>

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 org.apache.ojb.broker.Article in the repository.xml contains an entry declaring class CdArticle as a derived class:

<!-- Definitions for org.apache.ojb.ojb.broker.Article -->
   <class-descriptor
      class="org.apache.ojb.broker.Article"
      proxy="dynamic"
      table="Artikel"
   >
      <extent-class class-ref="org.apache.ojb.broker.BookArticle" />
      <extent-class class-ref="org.apache.ojb.broker.CdArticle" />
      ...
   </class-descriptor>
Mapping Inheritance Hierarchies

In the literature on object/relational mapping the problem of mapping inheritance hierarchies to RDBMS has been widely covered. 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
)

2. 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
)

3. Map each class to a distinct table, but do not 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
)

OJB provides direct support for all three approaches.
But it's currently not recommended to mix mapping strategies within the same hierarchy !
In the following we demonstrate 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 under OJB. 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.
The criterion for ojbConcreteClass is statically added to the query in class QueryFactory and it therefore appears in the select-statement for each extent. This means that mixing mapping strategies should be avoided.

There is sample code for this feature in the method org.apache.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 extent org.apache.ojb.broker.AB -->
   <class-descriptor class="org.apache.ojb.broker.AB">
      <extent-class class-ref="org.apache.ojb.broker.A" />
      <extent-class class-ref="org.apache.ojb.broker.B" />
   </class-descriptor>

<!-- Definitions for org.apache.ojb.broker.A -->
   <class-descriptor
      class="org.apache.ojb.broker.A"
      table="AB_TABLE"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="ojbConcreteClass"
         column="CLASS_NAME"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="someValue"
         column="VALUE_"
         jdbc-type="INTEGER"
      />
   </class-descriptor>

The column CLASS_NAME is used to store the concrete type of each object.

If you cannot provide such an additional column, but need to use some other means of indicating the type of each object you will require some additional programming:

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

protected ClassDescriptor selectClassDescriptor(Map row)
                                        throws PersistenceBrokerException
{
    // check if there is an attribute which tells us
    // which concrete class is to be instantiated
    FieldDescriptor concreteClassFD =
        m_cld.getFieldDescriptorByName(
            ClassDescriptor.OJB_CONCRETE_CLASS);

    if (concreteClassFD == null)
        return m_cld;
    else
    {
        try
        {
            String concreteClass = (String) row.get(
                                    concreteClassFD.getColumnName());
            if (concreteClass == null ||
                                    concreteClass.trim().length() == 0)
            {
                throw new PersistenceBrokerException(
                    "ojbConcreteClass field returned null or 0-length string");
            }
            else
            {
                concreteClass = concreteClass.trim();
            }
            ClassDescriptor result = m_cld.getRepository().
                                        getDescriptorFor(concreteClass);
            if (result == null)
            {
                result = m_cld;
            }
            return result;
        }
        catch (PBFactoryException e)
        {
            throw new PersistenceBrokerException(e);
        }
    }
}

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

row-reader="my.own.RowReaderImpl"

You will learn more about RowReaders in the next section.

Mapping Each Class to a Distinct Table

This is the most simple solution. Just write a complete ClassDescriptor for each class that contains FieldDescriptors for all of the attributes, including inherited attributes.

Mapping Classes on Multiple Joined Tables

Here are the definitions for the classes A and B:

public class A
{
    // primary key
    int id;
    // mapped to a column in A_TABLE
    int someValueFromA;
}

public class B extends A
{
    // id is primary key and serves also as foreign key referencing A.id
    int id;
    // mapped to a column in B_TABLE
    int someValueFromB;
}
The next code block contains the class-descriptors for the the classes A and B.
<!-- Definitions for org.apache.ojb.broker.A -->
   <class-descriptor
      class="org.apache.ojb.broker.A"
      table="A_TABLE"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="someValueFromA"
         column="VALUE_"
         jdbc-type="INTEGER"
      />
   </class-descriptor>

   <class-descriptor
      class="org.apache.ojb.broker.B"
      table="B_TABLE"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />

      <field-descriptor
         name="someValueFromB"
         column="VALUE_"
         jdbc-type="INTEGER"
      />

      <reference-descriptor name="super"
        class-ref="org.apache.ojb.broker.A">
        <foreignkey field-ref="id" />
      </reference-descriptor>

   </class-descriptor>

As you can see from this mapping we need a special reference-descriptor that advises OJB to load the values for the inherited attributes from class A by a JOIN using the (B.id == A.id) foreign key reference.

The name="super" is not used to address an actual attribute of the class B but as a marker keyword defining the JOIN to the baseclass.

Auto-update must be true (this is the default) to force insertion of A when inserting B. So you'd better not define any auto-settings for this reference-descriptor !

Be aware that this sample does not declare org.apache.ojb.broker.B to be an extent of org.apache.ojb.broker.A. Using extents here will lead to problems (instatiating the wrong class) because the primary key is not unique within the hiearchy defined in the repository.

Attributes from the super-class A can be used the same way as attributes of B when querying for B. No path-expression is needed in this case:

Criteria c = new Criteria();
c.addEqualTo("someValueFromA", new Integer(1));
c.addEqualTo("someValueFromB", new Integer(2));
Query q = QueryFactory.newQuery(B.class, c);
broker.getCollectionByQuery(q);

The above example is based on the assumption that the primary key attribute B.id and its underlying column B_TABLE.ID is also used as the foreign key attribute.

Now let us consider a case where B_TABLE contains an additional foreign key column B_TABLE.A_ID referencing A_TABLE.ID. In this case the layout for class B could look like follows:

public class B extends A
{
    // id is the primary key
    int id;

    // aID is the foreign key referencing A.id
    int aID;

    // mapped to a column in B_TABLE
    int someValueFromB;
}
The mapping for B will then look like follows:
   <class-descriptor
      class="org.apache.ojb.broker.B"
      table="B_TABLE"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />

      <field-descriptor
         name="aID"
         column="A_ID"
         jdbc-type="INTEGER"
      />

      <field-descriptor
         name="someValueFromB"
         column="VALUE_"
         jdbc-type="INTEGER"
      />

      <reference-descriptor name="super"
  	     class-ref="org.apache.ojb.broker.A">
         <foreignkey field-ref="aID" />
      </reference-descriptor>

   </class-descriptor>

The mapping now contains an additional field-descriptor for the aID attribute.

In the "super" reference-descriptor the foreignkey field-ref attribute had to be changed to "aID".

It is also possible to have the extra foreign key column B_TABLE.A_ID but without having a foreign key attribute in class B:

public class B extends A
{
    // id is the primary key
    int id;

    // mapped to a column in B_TABLE
    int someValueFromB;
}

We can use OJB's anonymous field feature to get everything working without the "aID" attribute. We keep the field-descriptor for aID, but declare it as an anonymous field. We just have to add an attribute access="anonymous" to the field-descriptor:

   <class-descriptor
      class="org.apache.ojb.broker.B"
      table="B_TABLE"
   >
      <field-descriptor
         name="id"
         column="ID"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />

      <field-descriptor
         name="aID"
         column="A_ID"
         jdbc-type="INTEGER"
         access="anonymous"
      />

      <field-descriptor
         name="someValueFromB"
         column="VALUE_"
         jdbc-type="INTEGER"
      />

      <reference-descriptor name="super"
  	     class-ref="org.apache.ojb.broker.A">
         <foreignkey field-ref="aID" />
      </reference-descriptor>

   </class-descriptor>
You can learn more about the anonymous fields feature in this howto.

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():

protected Object getObjectFromResultSet() throws PersistenceBrokerException
{
    if (itemProxyClass != null)
    {
        // provide m_row with primary key data of current row
        m_mif.getRowReader().
            readPkValuesFrom(m_rsAndStmt.m_rs, m_mif, m_row);
        // assert: m_row is filled with primary key values from db
        return getProxyFromResultSet();
    }
    else
    {
        // 0. provide m_row with data of current row
        m_mif.getRowReader().
            readObjectArrayFrom(m_rsAndStmt.m_rs, m_mif, m_row);
        // assert: m_row is filled from db

        // 1.read Identity
        Identity oid = getIdentityFromResultSet();
        Object result = null;

        // 2. check if Object is in cache. if so return cached version.
        result = cache.lookup(oid);
        if (result == null)
        {
            // 3. If Object is not in cache
            // materialize Object with primitive
            // attributes filled from current row
            result = m_mif.getRowReader().readObjectFrom(m_row, m_mif);
            // result may still be null!
            if (result != null)
            {
                cache.cache(oid, result);
                // fill reference and collection attributes
                ClassDescriptor cld = m_mif.getRepository().
                                getDescriptorFor(result.getClass());
                m_broker.retrieveReferences(result, cld);
                m_broker.retrieveCollections(result, cld);
            }
        }
        else
        {
            ClassDescriptor cld = m_mif.getRepository().
                                getDescriptorFor(result.getClass());
            m_broker.refreshRelationships(result, cld);
        }

        return result;
    }
}

This method first uses a RowReader to instantiate a new object array and to fill it with primitive attributes from the current ResultSet row.
The RowReader to be used for a Class can be configured in the XML repository with the attribute row-reader. If no RowReader is specified, the RowReaderDefaultImpl is used. The method readObjectArrayFrom(...) of this class looks like follows:

public void readObjectArrayFrom(ResultSet rs,
                                ClassDescriptor cld,
                                Map row)
{
    try
    {
        Collection fields = cld.getRepository().
                        getFieldDescriptorsForMultiMappedTable(cld);
        Iterator it = fields.iterator();
        while (it.hasNext())
        {
            FieldDescriptor fmd = (FieldDescriptor) it.next();
            FieldConversion conversion = fmd.getFieldConversion();
            Object val = JdbcAccess.getObjectFromColumn(rs, fmd);
            row.put(fmd.getColumnName() , conversion.sqlToJava(val));
        }
    }
    catch (SQLException t)
    {
        throw new PersistenceBrokerException(
            "Error reading from result set",t);
    }

}

In the second step OJB checks if there is already a cached version of the object to materialize. If so the cached instance is returned. If not, the object is fully materialized by first reading in primary attributes with the RowReader method readObjectFrom(Map row, ClassDescriptor descriptor) and in a second step by retrieving reference- and collection-attributes. The fully materilized Object is then returned.

public Object readObjectFrom(Map row, ClassDescriptor descriptor)
                    throws PersistenceBrokerException
{
    // allow to select a specific classdescriptor
    ClassDescriptor cld = selectClassDescriptor(row, descriptor);
    return buildWithReflection(cld, row);
}

By implementing your own RowReader you can hook into the OJB materialization process and provide additional features.

Rowreader Example

Assume that for some reason we do not 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. This approach is also called 'nested objects'. The section nested objects contains a different and much simpler approach to implement nested fields.

The class org.apache.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 org.apache.ojb.broker.RowReaderTestImpl extends the RowReaderDefaultImpl and overrides the readObjectFrom(...) method as follows:

public Object readObjectFrom(Map row, ClassDescriptor cld)
{
    Object result = super.readObjectFrom(row, cld);
    if (result instanceof ArticleWithStockDetail)
    {
        ArticleWithStockDetail art = (ArticleWithStockDetail) result;
        boolean sellout = art.isSelloutArticle;
        int minimum = art.minimumStock;
        int ordered = art.orderedUnits;
        int stock = art.stock;
        String unit = art.unit;
        StockDetail detail = new StockDetail(sellout, minimum,
                                    ordered, stock, unit, art);
        art.stockDetail = detail;
        return art;
    }
    else
    {
        return result;
    }
}

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

<class-descriptor
  class="org.apache.ojb.broker.ArticleWithStockDetail"
  table="Artikel"
  row-reader="org.apache.ojb.broker.RowReaderTestImpl"
>
Nested Objects

In the last section we discussed the usage of a user written RowReader to implement nested objects. This approach has several disadvantages.

  1. It is necessary to write code and to have some understanding of OJB internals.
  2. The user must take care that all nested fields are written back to the database on store.
This section shows that nested objects can be implemented without writing code, and without any further trouble just by a few settings in the repository.xml file.

The class org.apache.ojb.broker.ArticleWithNestedStockDetail has a stockDetail attribute, holding a reference to a StockDetail object. The class StockDetail is not declared in the XML repository as a first class entity class.

public class ArticleWithNestedStockDetail implements java.io.Serializable
{
    /**
     * this attribute is not filled through a reference lookup
     * but with the nested fields feature
     */
    protected StockDetail stockDetail;

    ...
}
The StockDetail class has the following layout:
public class StockDetail implements java.io.Serializable
{
    protected boolean isSelloutArticle;

    protected int minimumStock;

    protected int orderedUnits;

    protected int stock;

    protected String unit;

    ...
}
Only precondition to make things work is that StockDetail needs a default constructor.
The nested fields semantics can simply declared by the following class- descriptor:
   <class-descriptor
      class="org.apache.ojb.broker.ArticleWithNestedStockDetail"
      table="Artikel"
   >
      <field-descriptor
         name="articleId"
         column="Artikel_Nr"
         jdbc-type="INTEGER"
         primarykey="true"
         autoincrement="true"
      />
      <field-descriptor
         name="articleName"
         column="Artikelname"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="supplierId"
         column="Lieferanten_Nr"
         jdbc-type="INTEGER"
      />
      <field-descriptor
         name="productGroupId"
         column="Kategorie_Nr"
         jdbc-type="INTEGER"
      />
      <field-descriptor
         name="stockDetail::unit"
         column="Liefereinheit"
         jdbc-type="VARCHAR"
      />
      <field-descriptor
         name="price"
         column="Einzelpreis"
         jdbc-type="FLOAT"
      />
      <field-descriptor
         name="stockDetail::stock"
         column="Lagerbestand"
         jdbc-type="INTEGER"
      />
      <field-descriptor
         name="stockDetail::orderedUnits"
         column="BestellteEinheiten"
         jdbc-type="INTEGER"
      />
      <field-descriptor
         name="stockDetail::minimumStock"
         column="MindestBestand"
         jdbc-type="INTEGER"
      />
      <field-descriptor
         name="stockDetail::isSelloutArticle"
         column="Auslaufartikel"
         jdbc-type="INTEGER"
         conversion="org.apache.ojb.broker.accesslayer.conversions.Boolean2IntFieldConversion"
      />
   </class-descriptor>

That's all! Just add nested fields by using :: to specify attributes of the nested object. All aspects of storing and retrieving the nested object are managed by OJB.

Instance Callbacks

OJB does provide transparent persistence. That is, persistent classes do not need to implement an interface or extent a persistent baseclass.

For certain situations it may be neccesary to allow persistent instances to interact with OJB. This is supported by a simple instance callback mechanism.

The interface org.apache.ojb.PersistenceBrokerAware provides a set of methods that are invoked from the PersistenceBroker during operations on persistent instances:

public interface PersistenceBrokerAware
{
    /**
     * this method is called as the first operation within a call to
     * PersistenceBroker.store(Object pbAwareObject), if
     * the persistent object needs insert.
     */
    public void beforeInsert(PersistenceBroker broker)
                        throws PersistenceBrokerException;

    /**
     * this method is called as the last operation within a call to
     * PersistenceBroker.store(Object pbAwareObject), if
     * the persistent object needs insert.
     */
    public void afterInsert(PersistenceBroker broker)
                        throws PersistenceBrokerException;

    /**
     * this method is called as the first operation within a call to
     * PersistenceBroker.store(Object pbAwareObject), if
     * the persistent object needs update.
     */
    public void beforeUpdate(PersistenceBroker broker)
                        throws PersistenceBrokerException;

    /**
     * this method is called as the last operation within a call to
     * PersistenceBroker.store(Object pbAwareObject), if
     * the persistent object needs update.
     */
    public void afterUpdate(PersistenceBroker broker)
                        throws PersistenceBrokerException;

    /**
     * this method is called as the first operation within a call to
     * PersistenceBroker.delete(Object pbAwareObject).
     */
    public void beforeDelete(PersistenceBroker broker)
                        throws PersistenceBrokerException;

    /**
     * this method is called as the last operation within a call to
     * PersistenceBroker.delete(Object pbAwareObject).
     */
    public void afterDelete(PersistenceBroker broker)
                        throws PersistenceBrokerException;

    /**
     * this method is called as the last operation within a call to
     * PersistenceBroker.getObjectByXXX() or
     * PersistenceBroker.getCollectionByXXX().
     */
    public void afterLookup(PersistenceBroker broker)
                        throws PersistenceBrokerException;
}

If you want your persistent entity to perform certain operations after it has been stored by the PersistenceBroker you have to perform the following steps:

  1. let your persistent entity class implement the interface PersistenceBrokerAware.
  2. provide empty implementations for all required mthods.
  3. implement the method afterStore(PersistenceBroker broker) to perform your intended logic.

In the following you see code from a class DBAutoIncremented that does not use the OJB sequence numbering, but relies on a database specific implementation of autoincremented primary key values.
When the broker is storing such an instance the DB assigns an autoincrement value to the primary key column mapped to the attribute m_id. The afterStore(PersistenceBroker broker) instance callback is used to update the the attribute m_id with this value.

public abstract class DBAutoIncremented
                    implements PersistenceBrokerAware
{
    private static final String ID_ATTRIBUTE_NAME = "m_id";

    public void afterDelete(PersistenceBroker broker)
    {
    }

    public void afterLookup(PersistenceBroker broker)
    {
    }

    /**
     * after storing a new instance reflect the
     * autoincremented PK value
     * back into the PK attribute.
     */
    public void afterStore(PersistenceBroker broker)
    {
        try
        {
            // remove object from cache to ensure we are retrieving a
            // copy that is in sync with the database.
            broker.removeFromCache(this;)

            Class clazz = getClass();
            ClassDescriptor cld = broker.getClassDescriptor(clazz);
            PersistentField idField =
                cld
                    .getFieldDescriptorByName(ID_ATTRIBUTE_NAME)
                    .getPersistentField();

            if (hasNotBeenSet(idField))
            {

                // retrieve the object again with a query
                // on all non-id attributes.
                Object object =
                    broker.getObjectByQuery(
                        buildQueryOnAllNonIdAttributes(clazz, cld));

                if (object == null)
                {
                    throw new PersistenceBrokerException(
                        "cannot assign ID to "
                            + this
                            + " ("
                            + clazz
                            + ")"
                            + " because lookup by attributes failed");
                }

                // set id attribute with the value
                // assigned by the database.
                idField.set(this, idField.get(object));
            }
        }
    }

    public void beforeDelete(PersistenceBroker broker)
    {
    }

    public void beforeStore(PersistenceBroker broker)
    {
    }

    /**
     * returns a query that identifies an object by all its non-
     * primary key attributes.
     * this method is only safe, if these values are unique!
     */
    private Query buildQueryOnAllNonIdAttributes(
        Class clazz,
        ClassDescriptor cld)
    {

        // note: these are guaranteed to be in the same order
        FieldDescriptor[] fields = cld.getFieldDescriptions();
        Object[] values = cld.getAllValues(this);
        Criteria crit = new Criteria();

        for (int i = 0; i < fields.length; i++)
        {
            if (!fields[i].getAttributeName().
                            equals(ID_ATTRIBUTE_NAME))
            {
                if (values[i] == null)
                {
                    crit.addIsNull(fields[i].getAttributeName());
                }
                else
                {
                    crit.addEqualTo(fields[i].getAttributeName(),
                                                        values[i]);
                }
            }
        }
        return QueryFactory.newQuery(clazz, crit);
    }

    /**
     * returns true if attribute idField == 0,
     * else false.
     */
    private boolean hasNotBeenSet(PersistentField idField)
    {
        return (((Integer) idField.get(this)).intValue() == 0);
    }
}
Customizing collection queries

Customizing the query used for collection retrieval allows a developer to take full control of collection mechanism. For example only children having a certain attribute should be loaded. This is achieved by a QueryCustomizer defined in the collection-descriptor of a relationship:

<collection-descriptor
    name="allArticlesInGroup"
    ...
>
    <inverse-foreignkey field-ref="productGroupId"/>

    <query-customizer
    class="org.apache.ojb.broker.accesslayer.QueryCustomizerDefaultImpl">
        <attribute
            attribute-name="attr1"
            attribute-value="value1"
        />
    </query-customizer>

</collection-descriptor>

The query customizer must implement the interface org.apache.ojb.broker.accesslayer.QueryCustomizer. This interface defines the single method below which is used to customize (or completely rebuild) the query passed as argument. The interpretation of attribute-name and attribute-value read from the collection-descriptor is up to your implementation.

   /**
    * Return a new Query based on the original Query, the
    * originator object and the additional Attributes
    *
    * @param anObject the originator object
    * @param aBroker the PersistenceBroker
    * @param aCod the CollectionDescriptor
    * @param aQuery the original 1:n-Query
    * @return Query the customized 1:n-Query
    */
   public Query customizeQuery(Object anObject,
                    PersistenceBroker aBroker,
                    CollectionDescriptor aCod, Query aQuery);

The class org.apache.ojb.broker.accesslayer.QueryCustomizerDefaultImpl provides a default implentation without any functionality, it simply returns the query.


Copyright © 1999-2003, Apache Software Foundation