10.3.2. Defining the Object-Relational Mapping
OJB is configured through a number of files. The first, OJB.properties, controls all aspects of the system at the highest level but does not deal directly with any mapping information.OJB is highly configurable, and most of the classes that compose it are defined as interfaces or factories, making it easy to swap in custom implementations. For example, at some point between objects and the database OJB must generate SQL, and it uses a class called SqlGeneratorDefaultImpl to do so based on the beans to be retrieved and various constraints. The generated SQL should work well with most databases, but some may offer features that are not expressible in standard SQL. If this is the case, the SQL generator can be swapped for a custom one that knows about these features just by changing the SqlGeneratorClass= line in OJB.properties. Such enhancements are beyond the scope of the book, and OJB comes with a default set of definitions that will work quite well for a wide range of applications and environments.Much of the rest of OJB.properties is concerned with logging. OJB can be configured to report information about the SQL that has been generated, the results that have been returned, various internal activities, and a great deal more. All this configuration and logging happens through the Apache log4j APIs, which is discussed in Chapter 11.Of the remaining properties, only a few are of immediate interest:
repositoryFile=repository.xmlThe repositoryFile specifies the name of the XML file through which the mapping is defined. The other properties should look familiar because they define settings that were used in DBCP. This is no coincidence; OJB uses DBCP internally to cache connections. Thus, using OJB includes all the benefits of pooling for free.The repository.xml file contains all the information about the database and the mapping between tables and beans. There is a lot of information here, so for clarity the repository is typically split into three pieces, and repository.xml serves only to group these pieces together. A typical repository is shown in Listing 10.7.
maxIdle=-1
maxWait=2000
timeBetweenEvictionRunsMillis=-1
minEvictableIdleTimeMillis=1000000
whenExhaustedAction=0
Listing 10.7. The repository definition
This specifies a DTD for the file and each piece; indicates that the database entity should come from the repository_database.xml file, and similarly for the other pieces; and then includes all three entities. There is nothing special about either the entity or filenames, but the ones just used are descriptive and used in example programs that comes with OJB.The repository_database.xml file contains information relevant to the database connection, and it is shown in Listing 10.8. Recall that OJB.properties contains configuration information for DBCP but none of the underlying database information.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE descriptor-repository SYSTEM "repository.dtd" [
<!ENTITY database SYSTEM "repository_database.xml ">
<!ENTITY internal SYSTEM "repository_internal.xml ">
<!ENTITY user SYSTEM "repository_user.xml">
]>
<descriptor-repository version="1.0"
isolation-level="read-uncommitted">
&database;
&internal;
&user;
</descriptor-repository>
Listing 10.8. The database definition
<jdbc-connection-descriptorChapter 10" directory.All the interesting mappings between tables and objects happens in repository_user.xml. This file consists of a series of nodes associating objects to tables, and within these nodes are nodes mapping fields to columns. An entry mapping the primitive fields of the artist and album tables is shown in Listing 10.9.
jcd-alias="default"
default-connection="true"
platform="Hsqldb"
jdbc-level="2.0"
driver="org.hsqldb.jdbcDriver"
protocol="jdbc"
subprotocol="hsqldb"
dbalias="test"
username="sa"
password="
eager-release="false"
batch-mode="false"
useAutoCommit="1"
ignoreAutoCommitExceptions="false"
>
<connection-pool
maxActive="21"
validationQuery="/>
<sequence-manager
className="org.apache.ojb.broker.util.sequence.
SequenceManagerHighLowImpl">
<attribute attribute-name="grabSize"
attribute-value="20"/>
<attribute attribute-name="autoNaming"
attribute-value="true"/>
<attribute attribute-name="globalSequenceId"
attribute-value="false"/>
<attribute attribute-name="globalSequenceStart"
attribute-value="10000"/>
</sequence-manager>
</jdbc-connection-descriptor>
Listing 10.9. Mapping simple fields
Each class-descriptor node specifies the table and the implementing class. Each field-descriptor associates a column in the table with the name of a field in the class. The jdbc-type is also provided. There is no need to specify the type of the bean field because it can be obtained through introspection.The field-descriptor entries for artistId and albumId are marked as primary keys and configured to auto-increment. The auto-incrementing functionality ties into the sequence-manager noted in the repository_database.xml file in Listing 10.8 and will be discussed in the section on using OJB.The definition in Listing 10.9 is valid as it stands and could be used to create new artists and albums or to retrieve artist and album data by name or id. The next step is to add descriptors for the one-to-many and many-to-many relationships. Such relationships are called collections in the xml files.To define a collection OJB must know the name of the field in the parent class that will hold the children, the name of the class representing each child, and how the relationship is stored in the database. This information is packaged into a collection-descriptor node, which resides at the same level as the field-descriptor nodes within the class-descriptor. Adding the album information is done with the following descriptor:
<!-- Artist definition -->
<class-descriptor
table="ARTIST">
<field-descriptor
name="artistId"
column="ARTIST_ID"
jdbc-type="INTEGER"
primarykey="true"
autoincrement="true"
/>
<field-descriptor
name="name"
column="NAME"
jdbc-type="VARCHAR"
/>
</class-descriptor>
<!-- Album definition -->
<class-descriptor
table="ALBUM">
<field-descriptor
name="albumId"
column="ALBUM_ID"
jdbc-type="INTEGER"
primarykey="true"
autoincrement="true"
/>
<field-descriptor
name="name"
column="NAME"
jdbc-type="VARCHAR"
/>
<field-descriptor
name="yearReleased"
column="YEAR_RELEASED"
jdbc-type="INTEGER"
/>
</class-descriptor>
The name refers to the name of the property in the bean. The element-class-ref indicates the class attribute of the class-descriptor, which describes the child class and table. The orderby and sort attributes are not necessary, but generally it will make sense to build the collection in some order. In this case, these two attributes specify that the collection should be arranged in the order in which albums were released, with the first album at the start. Finally, the role of the three auto- attributes will be discussed in the section on the OJB APIs.The inverse-foreignkey indicates how to build the query by specifying both the name of the field in the child table and the name of the property in the current table to use as a value. Note that the Java version of the name is used, not the SQL name.For the albums belonging to the artist whose artist_id is 1, this will result in the following query:
<collection-descriptor
name="albums"
element-class-ref="com.awl.toolbook.chapter10.Album"
orderby="year_released"
sort="ASC"
auto-retrieve="true"
auto-update="true"
auto-delete="true"
>
<inverse-foreignkey field-ref="artistId"/>
</collection-descriptor>
For each row returned by this query a new Album will be constructed. Its fields will be set according to the rules in the class-descriptor, and a List will be constructed internally by OJB containing all such objects. Then the setAlbums() method of the parent Artist will be called with this List.There are two ways to handle many-to-many relationships. In one, the connection between the first table and the join table is treated as a one-to-many relationship, and the connection between the join table and the second table is treated as one-to-one.This is exemplified by the relationship between the artist and live_show tables. One artist may have many live_shows, and it is possible to obtain the venue from the live_show. The first step in defining these mappings is defining the descriptors for the venue and live_show tables, which is shown in Listing 10.10.
select * from album where artist_id=1
order by year_released asc;
Listing 10.10. The venue and live show descriptors
The descriptor for venue is straightforward, containing only primitive fields (not all of which are shown in the interests of space). It would be easy enough to add liveShows and artists references, from which it would be possible to get lists of all shows that had taken place at a particular location. These fields could be added to Venue in almost exactly the same way they are added to Artist; the details are left as an exercise for the reader.The descriptor for liveShow has three primitive values: artistId, venueId, and evenTDate. There are also two one-to-one mappings to obtain the Artist and Venue. These are defined with reference-descriptor nodes, which are similar to collection-descriptors, with the obvious difference that they only expect a single result.With these two definitions it is a simple matter to add the list of liveShows to the artist descriptor:
<class-descriptor
table="VENUE">
<field-descriptor
name="venueId"
column="VENUE_ID"
jdbc-type="INTEGER"
primarykey="true"
autoincrement="true"
/>
<field-descriptor
name="address1"
column="ADDRESS1"
jdbc-type="VARCHAR"
/>
</class-descriptor>
<!-- Live Show definition -->
<class-descriptor
table="LIVE_SHOW">
<field-descriptor
name="artistId"
column="ARTIST_ID"
jdbc-type="INTEGER"
/>
<field-descriptor
name="venueId"
column="VENUE_ID"
jdbc-type="INTEGER"
/>
<field-descriptor
name="eventDate"
column="EVENT_DATE"
jdbc-type="DATETIME"
/>
<!-- 1-1 references to objects -->
<reference-descriptor
name="venue"
class-ref="com.awl.toolbook.chapter11.Venue"
>
<foreignkey field-ref="venueId"/>
</reference-descriptor>
<reference-descriptor
name="artist"
class-ref="com.awl.toolbook.chapter11.Artist"
>
<foreignkey field-ref="artistId"/>
</reference-descriptor>
</class-descriptor>
This looks and functions exactly like the list of Albums.OJB also make it possible to bypass the join table entirely and go straight from an Artist to the list of Venues. This is done through a variation of the collection-descriptor that contains information about the intermediary join table and the foreign key connections to each table.
<collection-descriptor
name="liveShows"
element-class-ref="com.awl.toolbook.chapter10.LiveShow"
orderby="eventDate"
auto-retrieve="true"
auto-update="true"
auto-delete="true"
sort="ASC"
>
<inverse-foreignkey field-ref="artistId"/>
</collection-descriptor>
The indirection-table names the table, the fk-pointing-to-this-class indicates the field of the current table and by implication the property of the current class, and fk-pointing-to-element-class does the same for the elements of the collection, which is specified to be Venue by the element-class-ref. For the artist with artistId 1, this will result in the following query:
<collection-descriptor
name="venues"
element-class-ref="com.awl.toolbook.chapter10.Venue"
auto-retrieve="true"
auto-update="true "
auto-delete="true "
indirection-table="LIVE_SHOW"
>
<fk-pointing-to-this-class column="ARTIST_ID"/>
<fk-pointing-to-element-class column="VENUE_ID"/>
</collection-descriptor>
select venue.*
from venue,live_show
That completes coverage of the mapping features of OJB. There is more that can be done with this mapping, but between basic fields and one-to-one, one-to-many, and the two mechanisms for many-to-many relationships, the information provided previously should suffice for the great majority of purposes.A final word on developing with OJB: Writing beans and database definitions are typically reasonably straightforward activities, although both can have their subtleties. It should therefore not be surprising that often the most difficult part of starting an OJB-based program is getting the mapping file right. OJB is fairly good about reporting errors. A typo in a class or field name usually results in an error that describes the situations and reports the incorrect value. An error in the bean, such as the lack of a List for collections, results in an error that the collection could not be set and again provides the name of the collection in question. Problems in SQL, such as a nonexistent table or column, are reported as they come from the underlying database.However, there are cases where error messages are less helpful, such as certain introspection errors that result in mysterious null pointer exceptions. It may help when isolating these kinds of errors to remove field-descriptors one at a time until the problematic one is identified and then reviewing all the database and bean code related to that field.There may also be instances where the generated SQL is valid but does not do what is needed, in which case no error may be generated, but the results will be incorrect. Although the documentation states that all the generated SQL can be viewed by setting the appropriate log level, at the time of this writing this did not work, and there appears to be no way to view the executed SQL. Some JDBC drivers, such as MySQL provide their own mechanism for debugging SQL, which may help. If not, the best that can be done is to study the mapping file very closely and manually try to construct the SQL as OJB will. Or it would be possible to fix the SQL generator or provide an extended version that does log correctly. Another triumph for open source: When something doesn't work as desired, the in-house developers can fix it immediately.[2] There is no need to complain to a company that may not fix the problem until its next release, if ever.
where venue.venue_id=live_show.venue_id
and venue.artist_id=1
[2] Of course, if any such fix is done, the code should be sent to the OJB maintainers so they can make it available to all other users.