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.
|
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:
-
The attribute implementing the association (
name="productGroup" )
is productGroup.
-
The referenced object is of type (
class-ref="org.apache.ojb.broker.ProductGroup" )
org.apache.ojb.broker.ProductGroup .
-
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:
-
Add a private Article attribute named
sampleArticle
to the class ProductGroup .
-
Add a private int attribute named
sampleArticleId
to the ProductGroup class representing the foreign key.
-
Add a column
SAMPLE_ARTICLE_ID
INT to the table Kategorien .
-
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"
/>
|  |
 |  |  |
- 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 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>
|  |
 |  |  |
|
|
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:
-
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.)
-
auto-update="false"
On updating or inserting an object with
PersistenceBroker.store(...)
referenced objects are NOT updated by default.
-
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:
-
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.
-
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.
-
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>
|  |
 |  |  |
|
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)
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:
- two simple "one-class-only" extents, BookArticle and CdArticle.
- A compound extent Article containing all Article and BookArticle instances.
- 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:
- "one-class-only" extents require no declaration
- 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>
|  |
 |  |  |
- 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>
|  |
 |  |  |
|
|
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.
|
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:
-
let your persistent entity class implement the interface
PersistenceBrokerAware .
-
provide empty implementations for all required mthods.
-
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);
}
}
|  |
 |  |  |
|
|