Advanced O/R Mapping Technique
- Extents and Polymorphism
- Mapping Inheritance Hierarchies
- Using interfaces with OJB
- Change PersistentField Class
- How do anonymous keys work?
- Using Rowreader
- Nested Objects
- Instance Callbacks
- Manageable Collection
- Customizing collection queries
- Metadata runtime changes
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. There are many example classes for polymorphism in OJB's JUnit TestSuite.
To demonstrate/explain Extents and Polymorphism we will look at
a simple class hierarchy:
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 example method searches for all objects implementing
InterfaceArticle with an articleName equal to
Hamlet (provided that the object mapping is correct, details will described later).
The Collection is e.g 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); }
Of course it is also possible to define reference attributes of an interface or baseclass type. The example class Article has a reference attribute (1:1 reference) of type ProductGroup and this can be a concrete/abstract class or interface.
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. E.g. all
Article, BookArticle and CdArticles objects.
In the following example the 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( "The InterfaceArticle Extent objects: " +result); }
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 metadata mapping file. The classes from the above example require the following declarations:
- one-class-only extents require no declaration
- A declaration for the base class 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="false" table="Artikel" ... > <extent-class class-ref="org.apache.ojb.broker.BookArticle" /> ... </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.CdArticle" /> <!-- not needed to declare --> <!--<extent-class class-ref="org.apache.ojb.broker.BookArticle" />--> </class-descriptor>
No need to declare BookArticle here, because it's a declared sub class of Article, so it's implicit declared by Article extent.
Why is it necessary to explicitely declare which classes implement an
interface and which classes are derived from a base class?
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 (we don't recommend to use this feature it's bad design, but if
you don't have an alternative...).
As an example, you will find that the
ClassDescriptor of abstract test class
org.apache.ojb.broker.CollectionTest$BookShelfItem in the
OJB's Test Suite contains an
entry declaring class org.apache.ojb.broker.CollectionTest$Candie as a derived class:
<class-descriptor class="org.apache.ojb.broker.CollectionTest$BookShelfItem"> <extent-class class-ref="org.apache.ojb.broker.CollectionTest$Book"/> <extent-class class-ref="org.apache.ojb.broker.CollectionTest$DVD"/> <!-- This class isn't a subclass of Book or DVD or a implementation of BookShelfItem, anyway it's possible to declare it as extent (but not recommended) --> <extent-class class-ref="org.apache.ojb.broker.CollectionTest$Candie"/> </class-descriptor>
Performance Tip
When using extents OJB will produce some overhead for each declared
extent (e.g. execute a separate select-query for each extent or using complex table joins).
Thus it's important to avoid unnecessary extent declarations. If in the above
example class InterfaceArticle is never
used in queries, don't declare the extents for the implementing classes
(Article, CdArticle). It's always possible to add additional
extents in mapping files.
Mapping Inheritance Hierarchies
In the literature on object/relational mapping the problem of mapping inheritance hierarchies to RDBMS has been widely covered. In the following sections we will use a simple inheritance example to show the different inheritance mapping strategies.
Assume we have a base class Employee and class Executive extends Employee. Further on class Manager extends Executive.
If we have to define database tables that have to contain these classes we have to choose one of the following solutions:
-
Map each class of a hierarchy to a distinct table and have all attributes from the base class in the derived class.
-
Map subclass fields of a hierarchy to a distinct table, but do not map super class fields to derived classes. Use joins to materialize over all tables to materialize objects.
OJB provides direct support for all three approaches.
In the following we demonstrate how these mapping approaches can be implemented by using OJB.
Mapping Each Class of a Hierarchy to a Distinct Table (table per class)
This is the most simple solution. Just write a complete ClassDescriptor with FieldDescriptors for all of the attributes, including inherited attributes.
The classes of our mapping example would look like:
public class Employee implements Serializable { private Integer id; private String name; public Employee() { } .... // getter/setter for id and ojbConcreteClass } public class Executive extends Employee { private String department; .... // getter/setter } public class Manager extends Executive { private int consortiumKey; .... // getter/setter }
The ClassDescriptors include all fields of the representing java-class and each descriptor points to a different table:
<class-descriptor class="Employee" table="EMPLOYEE" > <extent-class class-ref="Executive" /> <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="name" column="NAME" jdbc-type="VARCHAR" /> </class-descriptor> <class-descriptor class="Executive" table="EXECUTIVE" > <extent-class class-ref="Manager" /> <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="name" column="NAME" jdbc-type="VARCHAR" /> <field-descriptor name="department" column="DEPARTMENT" jdbc-type="VARCHAR" /> </class-descriptor> <class-descriptor class="Manager" table="MANAGER" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="name" column="NAME" jdbc-type="VARCHAR" /> <field-descriptor name="department" column="DEPARTMENT" jdbc-type="VARCHAR" /> <field-descriptor name="consortiumKey" column="CONSORTIUM_KEY" jdbc-type="INTEGER" /> </class-descriptor>
The extent-class element is needed to declare the inheritance between the classes.
The DDL for the tables would look like:
CREATE TABLE EMPLOYEE ( ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(150) ) CREATE TABLE EXECUTIVE ( ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(150), DEPARTMENT VARCHAR(150) ) CREATE TABLE MANAGER ( ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(150), DEPARTMENT VARCHAR(150), CONSORTIUM_KEY INT )
Mapping Class Hierarchy on the Same Table (table per hierarchy)
Mapping several classes on one table works well under OJB. There is only one special situation that needs some attention:
Storing Employee, Executive and Manager objects to this table works fine. But now consider a Query against the baseclass Employee. How can the correct type of the stored objects be determined?
OJB needs a discriminator column of type CHAR or VARCHAR that contains the class name 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 classes of our mapping example would look like:
public class Employee implements Serializable { private Integer id; /** * This special attribute telling OJB which concrete class * this Object has. * NOTE: this attribute MUST be called ojbConcreteClass */ private String ojbConcreteClass; private String name; public Employee() { // this guarantee that always the correct class name will be set this.ojbConcreteClass = this.getClass().getName(); } .... // getter/setter for id and ojbConcreteClass } public class Executive extends Employee { private String department; public Executive() { super(); } .... // getter/setter } public class Manager extends Executive { private int consortiumKey; public Manager() { super(); } .... // getter/setter }
Here are the metadata mappings of our mapping example:
<class-descriptor class="Employee" table="MANPOWER" > <extent-class class-ref="Executive" /> <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="name" column="NAME" jdbc-type="VARCHAR" /> </class-descriptor> <class-descriptor class="Executive" table="MANPOWER" > <extent-class class-ref="Manager" /> <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="name" column="NAME" jdbc-type="VARCHAR" /> <field-descriptor name="department" column="DEPARTMENT" jdbc-type="VARCHAR" /> </class-descriptor> <class-descriptor class="Manager" table="MANPOWER" > <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="name" column="NAME" jdbc-type="VARCHAR" /> <field-descriptor name="department" column="DEPARTMENT" jdbc-type="VARCHAR" /> <field-descriptor name="consortiumKey" column="CONSORTIUM_KEY" jdbc-type="INTEGER" /> </class-descriptor>
The column CLASS_NAME is used to store the concrete type of each object.
The extent-class element is needed to declare the inheritance between the classes.
The DDL for the table would look like:
CREATE TABLE MANPOWER ( ID INT NOT NULL PRIMARY KEY, CLASS_NAME VARCHAR(150) NAME VARCHAR(150), DEPARTMENT VARCHAR(150), CONSORTIUM_KEY INT )
Implement your own Discriminator Handling
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 override 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 ClassDescriptor result = m_cld; Class ojbConcreteClass = (Class) row.get(OJB_CONCRETE_CLASS_KEY); if(ojbConcreteClass != null) { result = m_cld.getRepository().getDescriptorFor(ojbConcreteClass); // if we can't find class-descriptor for concrete // class, something wrong with mapping if (result == null) { throw new PersistenceBrokerException( "Can't find class-descriptor for ojbConcreteClass '" + ojbConcreteClass + "', the main class was " + m_cld.getClassNameOfObject()); } } return result; }
After implementing your own RowReader you must edit the ClassDescriptor for the respective class in the XML repository to specify the usage of your RowReader Implementation:
<class-descriptor class="my.Object" table="MY_OBJECT" ... row-reader="my.own.RowReaderImpl" ... > ...
You will learn more about RowReaders in this section.
Mapping Each Subclass to a Distinct Table (table per subclass)
This mapping strategy maps all subclass fields of a hierarchy to a distinct table (but do not map super class fields to derived class tables - except the primary key fields) and use joins to materialize over all tables to materialize the objects.
The classes of the inheritance hierarchy don't need any specific fields or settings, thus our mapping example java-classes look would look like the classes for the table-per-class mapping.
The next code block contains the class-descriptors of our mapping example.
<class-descriptor class="Employee" table="EMPLOYEE" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="name" column="NAME" jdbc-type="VARCHAR" /> </class-descriptor> <class-descriptor class="Executive" table="EXECUTIVE" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" /> <field-descriptor name="department" column="DEPARTMENT" jdbc-type="VARCHAR" /> <reference-descriptor name="super" class-ref="Employee" > <foreignkey field-ref="id"/> </reference-descriptor> </class-descriptor> <class-descriptor class="Manager" table="MANAGER" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" /> <field-descriptor name="consortiumKey" column="CONSORTIUM_KEY" jdbc-type="INTEGER" /> <reference-descriptor name="super" class-ref="Executive" > <foreignkey field-ref="id"/> </reference-descriptor> </class-descriptor>
The mapping for base class Employee is ordinary and we using
a autoincrement primary key field.
In the subclasses Executive and Manager it's not allowed
to use autoincrement primary keys, because OJB will automatically copy the
primary keys of the base class to all subclasses.
As you can see this mapping needs a special
reference-descriptor
in the subclasses Executive and Manager that advises OJB to load
the values for the inherited attributes from the super-class by a JOIN using the
foreign key reference.
The name="super" attribute is not used to address an actual
attribute of the super-class but as a marker keyword defining the JOIN
to the super-class.
The DDL for the tables would look like:
CREATE TABLE EMPLOYEE ( ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(150) ) CREATE TABLE EXECUTIVE ( ID INT NOT NULL PRIMARY KEY, DEPARTMENT VARCHAR(150) ) CREATE TABLE MANAGER ( ID INT NOT NULL PRIMARY KEY, CONSORTIUM_KEY INT )
Attributes from the base- or superclasses can be used the same way as attributes of the target class when querying - e.g. for Executive or Manager. No path-expression is needed in this case. The following examples returns all Executive and Manager matching the criteria:
Criteria c = new Criteria(); // attribute defined in base class Employee c.addEqualTo("name", "Kent"); // attribute defined in Executive c.addEqualTo("department", "press"); Query q = QueryFactory.newQuery(Executive.class, c); // returns all matching Executive and Manager instances Collection result = broker.getCollectionByQuery(q);
Table Per Subclass via Foreign Key
The above example is based on the assumption that the primary key attribute Employee.id and its underlying column EMPLOYEE.ID is also used as the foreign key attribute in the the subclasses.
Now let us consider a case where this is not possible, then it's possible to use an additional foreign key field/column in the subclass referencing the base-/superclass.
In this case the layout for class Executive would need an
additional field employeeFk to store the foreign key reference
to Employee.
To avoid the additional field in the subclass (if desired) we can use OJB's
anonymous field feature
to get everything working without the employeeFk attribute in subclass
Employee (thus the java classes of our
mapping example). We keep the
field-descriptor for employeeFk,
but declare it as an anonymous field.
We just have to add an attribute
access="anonymous" to the new field-descriptor employeeFk:.
<class-descriptor class="Employee" table="EMPLOYEE" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="name" column="NAME" jdbc-type="VARCHAR" /> </class-descriptor> <class-descriptor class="Executive" table="EXECUTIVE" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="department" column="DEPARTMENT" jdbc-type="VARCHAR" /> <field-descriptor name="employeeFk" column="EMPLOYEE_FK" jdbc-type="INTEGER" access="anonymous" /> <reference-descriptor name="super" class-ref="Employee" > <foreignkey field-ref="employeeFk"/> </reference-descriptor> </class-descriptor> <class-descriptor class="Manager" table="MANAGER" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="consortiumKey" column="CONSORTIUM_KEY" jdbc-type="INTEGER" /> <field-descriptor name="executiveFk" column="EXECUTIVE_FK" jdbc-type="INTEGER" access="anonymous" /> <reference-descriptor name="super" class-ref="Executive" > <foreignkey field-ref="executiveFk"/> </reference-descriptor> </class-descriptor>
Now it's possible to use autoincrement primary key fields in all classes
of the hierarchy (because they are decoupled from the inheritance references).
The foreignkey-element have to refer the new (anomymous)
foreign-key field.
Using interfaces with OJB
Sometimes you may want to declare class descriptors for interfaces rather than for concrete classes. With OJB this is no problem, but there are a couple of things to be aware of, which are detailed in this section.
Consider this example hierarchy :
public interface A { String getDesc(); } public class B implements A { /** primary key */ private Integer id; /** sample attribute */ private String desc; public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } } public class C { /** primary key */ private Integer id; /** foreign key */ private Integer aId; /** reference */ private A obj; public void test() { String desc = obj.getDesc(); } }
Here, class C references the interface A rather than B. In order to make this work with OJB, four things must be done:
- All features common to all implementations of A are declared in the class descriptor of A. This includes references (with their foreignkeys) and collections.
- Since interfaces cannot have instance fields, it is necessary to use bean properties instead. This means that for every field (including collection fields), there must be accessors (a get method and, if the field is not marked as access="readonly", a set method) declared in the interface.
- Since we're using bean properties, the appropriate
org.apache.ojb.broker.metadata.fieldaccess.PersistentField
implementation must be used (see below).
This class is used by OJB to access the fields when storing/loading objects. Per default,
OJB uses a direct access implementation
(org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldDirectImpl) which
requires actual fields to be present.
In our case, we need an implementation that rather uses the accessor methods. Since the PersistentField setting is (currently) global, you have to check whether there are accessors defined for every field in the metadata. If yes, then you can use the org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldIntrospectorImpl, otherwise you'll have to resort to the org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldAutoProxyImpl, which determines for every field what type of field it is and then uses the appropriate strategy. - If at some place OJB has to create an object of the interface, say as the result type of a query, then you have to specify factory-class and factory-method for the interface. OJB then uses the specified class and (static) method to create an uninitialized instance of the interface.
In our example, this would result in:
public interface A { void setId(Integer id); Integer getId(); void setDesc(String desc); String getDesc(); } public class B implements A { /** primary key */ private Integer id; /** sample attribute */ private String desc; public String getId() { return id; } public void setId(Integer id) { this.id = id; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } } public class C { /** primary key */ private Integer id; /** foreign key */ private Integer aId; /** reference */ private A obj; public void test() { String desc = obj.getDesc(); } } public class AFactory { public static A createA() { return new B(); } }
The class descriptors would look like:
<class-descriptor class="A" table="A_TABLE" factory-class="AFactory" factory-method="createA" > <extent-class class-ref="B"/> <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="desc" column="DESC" jdbc-type="VARCHAR" length="100" /> </class-descriptor> <class-descriptor class="B" table="B_TABLE" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="desc" column="DESC" jdbc-type="VARCHAR" length="100" /> </class-descriptor> <class-descriptor class="C" table="C_TABLE" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="aId" column="A_ID" jdbc-type="INTEGER" /> <reference-descriptor name="obj" class-ref="A"> <foreignkey field-ref="aId" /> </reference-descriptor> </class-descriptor>
One scenario where you might run into problems is the use of interfaces for nested objects. In the above example, we could construct such a scenario if we remove the descriptors for A and B, as well as the foreign key field aId from class C and change its class descriptor to:
<class-descriptor class="C" table="C_TABLE" > <field-descriptor name="id" column="ID" jdbc-type="INTEGER" primarykey="true" autoincrement="true" /> <field-descriptor name="obj::desc" column="DESC" jdbc-type="VARCHAR" length="100" /> </class-descriptor>
The access to desc will work because of the usage of bean properties,
but you will get into trouble when using
dynamic proxies for C.
Upon materializing an object of type C, OJB will try to create
the instance for the field obj which is of type A.
Of course, this is an interface but OJB won't check whether there is class
descriptor for the type of obj (in fact there does not have to
be one, and usually there isn't) because obj is not defined as
a reference. As a result, OJB tries to instantiate an interface, which of
course fails.
Currently, the only way to handle this is to write a
custom invocation handler
that knows how to create an object of type A.
Change PersistentField Class
OJB supports a pluggable strategy to read and set the persistent attributes in the persistence capable classes. All strategy implementation classes have to implement the interface org.apache.ojb.broker.metadata.fieldaccess.PersistentField. OJB provide a few implementation classes which can be set in OJB.properties file:
# The PersistentFieldClass property defines the implementation class # for PersistentField attributes used in the OJB MetaData layer. # By default the best performing attribute/refection based implementation # is selected (PersistentFieldDirectAccessImpl). # # - PersistentFieldDirectAccessImpl # is a high-speed version of the access strategies. # It does not cooperate with an AccessController, # but accesses the fields directly. Persistent # attributes don't need getters and setters # and don't have to be declared public or protected # - PersistentFieldPrivilegedImpl # Same as above, but does cooperate with AccessController and do not # suppress the java language access check (but is slow compared with direct access). # - PersistentFieldIntrospectorImpl # uses JavaBeans compliant calls only to access persistent attributes. # No Reflection is needed. But for each attribute xxx there must be # public getXxx() and setXxx() methods. # - PersistentFieldDynaBeanAccessImpl # implementation used to access a property from a # org.apache.commons.beanutils.DynaBean. # - PersistentFieldAutoProxyImpl # for each field determines upon first access how to access this particular field # (directly, as a bean, as a dyna bean) and then uses that strategy # #PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldDirectImpl #PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldPrivilegedImpl #PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldIntrospectorImpl #PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldDynaBeanImpl #PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldAutoProxyImpl #(DynaBean implementation does not support nested fields) #
E.g. if the PersistentFieldDirectImpl is used there must be an attribute in the persistent class with this name, if the PersistentFieldIntrospectorImpl is used there must be a JavaBeans compliant property of this name. More info about the individual implementation can be found in javadoc.
How do anonymous keys work?
To play for safety it is mandatory to understand how this feature is working. In the HOWTO section is detailed described how to use anoymous keys.
All involved classes can be found in org.apache.ojb.broker.metadata.fieldaccess package. The
classes used for anonymous keys start with a AnonymousXYZ.java prefix.
Main class used for provide anonymous keys is
org.apache.ojb.broker.metadata.fieldaccess.AnonymousPersistentField. Current implementation
use an object identity based weak HashMap. The persistent object identity is used as key for the
anonymous key value. The (Anonymous)PersistentField instance is associated with the FieldDescriptor
declared in the repository.
This means that all anonymous key information will be lost when the object identity change, e.g. the persistent object will be de-/serialized or copied. In conjuction with 1:1 references this will be no problem, because OJB can use the referenced object to re-create the anonymous key information (FK to referenced object).
Using Rowreader
RowReaders provide a callback mechanism that allows to interact with the OJB load mechanism. All implementation classes have to implement interface RowReader.
You can specify the RowReader implementation in
- the OJB.properties file to set the standard used RowReader implementation
#------------------------------------------------------------------------------- # RowReader #------------------------------------------------------------------------------- # Set the standard RowReader implementation. It is also possible to specify the # RowReader on class-descriptor level. RowReaderDefaultClass=org.apache.ojb.broker.accesslayer.RowReaderDefaultImpl
- within the class-descriptor to set the RowReader for a specific class.
RowReader setting on class-descriptor level will override the standard reader set in OJB.properties file. If neither a RowReader was set in OJB.properties file nor in class-descriptor was set, OJB use an default implementation.
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 method 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 (getItemProxyClass() != null) { // provide m_row with primary key data of current row getQueryObject().getClassDescriptor().getRowReader() .readPkValuesFrom(getRsAndStmt().m_rs, getRow()); // assert: m_row is filled with primary key values from db return getProxyFromResultSet(); } else { // 0. provide m_row with data of current row getQueryObject().getClassDescriptor().getRowReader() .readObjectArrayFrom(getRsAndStmt().m_rs, getRow()); // 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 = getCache().lookup(oid); if (result == null) { // 3. If Object is not in cache // materialize Object with primitive attributes filled from // current row result = getQueryObject().getClassDescriptor() .getRowReader().readObjectFrom(getRow()); // result may still be null! if (result != null) { synchronized (result) { getCache().enableMaterializationCache(); getCache().cache(oid, result); // fill reference and collection attributes ClassDescriptor cld = getQueryObject().getClassDescriptor() .getRepository().getDescriptorFor(result.getClass()); // don't force loading of reference final boolean unforced = false; // Maps ReferenceDescriptors to HashSets of owners getBroker().getReferenceBroker().retrieveReferences(result, cld, unforced); getBroker().getReferenceBroker().retrieveCollections(result, cld, unforced); getCache().disableMaterializationCache(); } } } else // Object is in cache { ClassDescriptor cld = getQueryObject().getClassDescriptor() .getRepository().getDescriptorFor(result.getClass()); // if refresh is required, update the cache instance from the db if (cld.isAlwaysRefresh()) { getQueryObject().getClassDescriptor() .getRowReader().refreshObject(result, getRow()); } getBroker().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 standard RowReader 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.
- It is necessary to write code and to have some understanding of OJB internals.
- 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:
Example
If you want that all persistent objects take care of CRUD operations performed by the PersistenceBroker you have to do the following steps:
- let your persistent entity class implement the interface PersistenceBrokerAware.
- provide empty implementations for all required mthods.
- implement the method afterUpdate(PersistenceBroker broker), afterInsert(PersistenceBroker broker) and afterDelete(PersistenceBroker broker) to perform your intended logic.
In the following "for demonstration only code" you see a class BaseObject (all persistent objects extend this class) that does send a notification using a messenger object after object state change.
public abstract class BaseObject implements PersistenceBrokerAware { private Messenger messenger; public void afterInsert(PersistenceBroker broker) { if(messenger != null) { messenger.send(this.getClass + " Object insert"); } } public void afterUpdate(PersistenceBroker broker) { if(messenger != null) { messenger.send(this.getClass + " Object update"); } } public void afterDelete(PersistenceBroker broker) { if(messenger != null) { messenger.send(this.getClass + " Object deleted"); } } public void afterLookup(PersistenceBroker broker){} public void beforeDelete(PersistenceBroker broker){} public void beforeStore(PersistenceBroker broker){} public void setMessenger(Messenger messenger) { this.messenger = messenger; } }
Manageable Collection
In 1:n or m:n relations, OJB can handle java.util.Collection as well as user defined collection classes as collection attributes in persistent classes. See collection-descriptor.collection-class attribute for more information.
In order to collaborate with the OJB mechanisms these collection must provide a minimum protocol as defined by this interface org.apache.ojb.broker.ManageableCollection.
public interface ManageableCollection extends java.io.Serializable { /** * 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. */ void ojbAdd(Object anObject); /** * adds a Collection to this collection. Used in reading Extents from the * Database. Thus it is save to cast otherCollection to this.getClass(). */ void ojbAddAll(ManageableCollection otherCollection); /** * returns an Iterator over all elements in the collection. Used during store and * delete Operations. * If the implementor does not return an iterator over ALL elements, OJB cannot * store and delete all elements properly. */ Iterator ojbIterator(); /** * A callback method to implement 'removal-aware' (track removed objects and delete * them by its own) collection implementations. */ public void afterStore(PersistenceBroker broker) throws PersistenceBrokerException; }
The methods have a prefix "ojb" that indicates that these methods are "technical" methods, required by OJB and not to be used in business code.
In package org.apache.ojb.broker.util.collections can be found a bunch of pre-defined implementations of org.apache.ojb.broker.ManageableCollection.
More info about which collection class to used here.
Types Allowed for Implementing 1:n and m:n Associations
OJB supports different Collection types to implement 1:n and m: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:
- java.util.Collection, java.util.List, java.util.Vector as in the example above. Internally OJB uses java.util.Vector to implement collections.
- Arrays (see the file ProductGroupWithArray).
-
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 and the typed collection class must be declared in the collection-descriptor using the collection-class attribute. ManageableCollection 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(); } }
And the collection-descriptor have to declare this class:
<collection-descriptor name="allArticlesInGroup" element-class-ref="org.apache.ojb.broker.Article" collection-class="org.apache.ojb.broker.ArticleCollection" auto-retrieve="true" auto-update="false" auto-delete="true" > <inverse-foreignkey field-ref="productGroupId"/> </collection-descriptor>
Which collection-class type should be used?
Earlier in this section the org.apache.ojb.broker.ManageableCollection was introduced. Now we talk about which type to use.
By default OJB use a removal-aware collection implementation.
These implementations (classes prefixed with Removal...) track
removal and addition of elements.
This tracking allow the PersistenceBroker
to delete elements from the database that have been
removed from the collection before a PB.store() operation occurs.
This default behaviour is undesired in some cases:
- In m:n relations, e.g. between Movie and Actor class. If an Actor was removed from the Actor collection of a Movie object expected behaviour was that the Actor be removed from the indirection table, but not the Actor itself. Using a removal aware collection will remove the Actor too. In that case a simple manageable collection is recommended by set e.g. collection-class="org.apache.ojb.broker.util.collections.ManageableArrayList" in collection-descriptor.
- In 1:n relations when the n-side objects be removed from the collection of the main object, but we don't want to remove them itself (be careful with this, because the FK entry of the main object still exists - more info about linking here).
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.
Metadata runtime changes
This was described in metadata section.
by Thomas Mahler, Jakob Braeuchli, Armin Waibel