|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Learn the basics of object-relational data binding using
Castor
Bruce
Snyder (mailto:ferret@frii.com?cc=&subject=Get
started with Castor JDO) Senior Software Engineer,
DigitalGlobe 1 August 2002
A growing number of enterprise projects
today call for a reliable method of binding Java objects to relational
data -- and doing so across a multitude of relational databases.
Unfortunately (as many of us have learned the hard way) in-house
solutions are painful to build and even harder to maintain and grow over
the long term. In this article, Bruce Snyder introduces you to the
basics of working with Castor JDO, an open source data-binding framework
that just happens to be based on 100 percent pure Java
technology.
Castor JDO (Java Data Objects) is an open source, 100 percent Java data
binding framework. Initially released in December 1999, Castor JDO was one
of the first open source data binding frameworks available. Since that
time, the technology has come a long way. Today, Castor JDO is used in
combination with many other technologies, both open source and commercial,
to bind Java object models to relational databases, XML documents, and
LDAP directories.
In this article, you'll learn the fundamentals of working with Castor
JDO. We'll start with a relational data model and a Java object model, and
discuss the basics of mapping between the two. From there, we'll talk
about some features of Castor JDO. Using a simple product-based example,
you'll learn about such essentials as inheritance (both relational and
object-oriented), dependent and related relationships, Castor's Object
Query Language implementation, and short versus long transactions in
Castor. Because this article is an introduction to Castor, we'll use very
simple examples here, and we won't go into depth on any one topic. At the
end of the article, you will have a good overview of the technology, and a
good foundation for future exploration.
Please note that this article will not go into the general topic of
object-relational mapping. To learn more about object-relational mapping,
see the Resources
section.
If you're interested in seeing how Castor JDO compares to the Sun JDO
specification, see "How
does Castor differ from the Sun JDO spec?."
Getting started Sometimes I
prefer to start a project by modeling the data; other times I like to
begin by modeling the objects. For the purposes of this article, we'll
start with the data model. Castor comes with some JDO examples that
present a data model and an object model surrounding the notion of a
product. We'll work with one of those examples, taking it through several
different stages throughout the article. Figure 1 is an
entity-relationship (ER) diagram of the data model for our example. For
the sake of simplicity, it contains no explicit foreign keys. Notice,
however, that the ID references do exist between tables.
Get the source
The Castor
Project's source code has been released to the Java technology
community via a BSD-style license called the ExoLab license. See Resources
to download Castor JDO. |
Figure 1. The Castor JDO example data
model
Figure 2 is a UML class diagram of the object model for the example.
The JDO examples provide a one-to-one representation of tables to objects.
Figure 2. The Castor JDO example object
model
Castor's Java objects look a lot like JavaBeans components. Therefore,
an object typically contains a pair of accessor and mutator
(getter/setter) methods for each property (unless the property is mapped
to be accessed directly). More complex relationships can contain
additional logic for other purposes. Object-relational mapping can become
extremely complex very fast; however, these examples are pretty
straightforward in nature.
Listing 1 shows the source code for the Product object.
Note that the code contains a property for each column in the product
table, including the identity. For those new to object-relational mapping,
a property for the identity may seem odd, but this construct is actually
quite common and is used in other object-relational frameworks as well.
Internally, Castor uses the object identity for tracking objects.
Additionally, there are two java.util.Vector s for related
ProductDetail and Category objects. Also note
that the object contains a toString() method that can be
useful for logging purposes when debugging your application. Listing 1. Product.java
package myapp;
import java.util.Vector;
import java.util.Enumeration;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.Persistent;
public class Product implements Persistent
{
private int _id;
private String _name;
private float _price;
private ProductGroup _group;
private Database _db;
private Vector _details = new Vector();
private Vector _categories = new Vector();
public int getId()
{
return _id;
}
public void setId( int id )
{
_id = id;
}
public String getName()
...
public void setName( String name )
...
public float getPrice()
...
public void setPrice( float price )
...
public ProductGroup getGroup()
...
public void setGroup( ProductGroup group )
...
public ProductDetail createDetail()
{
return new ProductDetail();
}
public Vector getDetails()
{
return _details;
}
public void addDetail( ProductDetail detail )
{
_details.add( detail );
detail.setProduct( this );
}
public Vector getCategories()
{
return _categories;
}
public void addCategory( Category category )
{
if ( _categories.contains( category ) ) {
_categories.addElement( category ); category.addProduct( this );
}
}
...
public String toString()
{
return _id + " " + _name;
}
}
|
Looking at Figure
2, note that Product has different relationships to the
ProductDetail and Category objects:
- One
Product can contain many
ProductDetail s. This is a one-to-many relationship.
- Many
Product s can contain many Category
objects. This is a many-to-many relationship.
The methods addDetail() and addCategory() are
used to add objects to each of the java.util.Vector s to
manage their respective relationships.
The karma of the mapping
descriptor Just as some people believe that the key to life
is a proper understanding and acceptance of one's personal karma, I've
found that the key to working with Castor JDO is a proper understanding
and implementation of the mapping descriptor. The mapping descriptor
provides the connection (the map) between relational database tables and
the Java objects. The format of the mapping descriptor is XML -- and for
good reason. The other half of the Castor Project is Castor XML (see Resources).
Castor XML provides a superior Java-to-XML and XML-to-Java data binding
framework. Castor JDO makes use of Castor XML's ability to unmarshall an
XML document into a Java object model for the purposes of reading the
mapping descriptor.
Mapping objects and properties to
elements and attributes Each Java object is represented by a
<class> element and each property in that object is
represented by a <field> element. Additionally, each
column from within a relational table is represented by an
<sql> element. Listing 2 shows the mapping for the
Product object seen above. Take a look at the listing and
then we'll discuss some of the finer points of the code. Listing 2. The mapping for Product
<class name="myapp.Product" identity="id">
<description>Product definition</description>
<map-to table="prod" xml="product" />
<field name="id" type="integer">
<sql name="id" type="integer" />
</field>
<field name="name" type="string">
<sql name="name" type="char" />
</field>
<field name="price" type="float">
<sql name="price" type="numeric" />
</field>
<!-- Product has reference to ProductGroup,
many products may reference same group -->
<field name="group" type="myapp.ProductGroup">
<sql name="group_id" />
</field>
<!-- Product has reference to ProductDetail
many details per product -->
<field name="details" type="myapp.ProductDetail" required="true"
collection="vector">
<sql many-key="prod_id"/>
</field>
<!-- Product has reference to Category with
many-many relationship -->
<field name="categories" type="myapp.Category" required="true"
collection="vector">
<sql name="category_id"
many-table="category_prod" many-key="prod_id" />
</field>
</class>
|
The <class> element supports some important
attributes and elements. For example, the mapping for Product
uses the identity attribute to indicate which property of the
object serves as the object identity. The <class>
element also supports a <map-to> element that tells
Castor to what relational table each object maps. The
<field> element supports some attributes as well.
Notice that the mapping for all the <field> and
<sql> elements contains a type attribute. Type
attributes indicate to Castor what TypeConvertor should be
used internally to convert between object and relational data types.
Defining relationships A
special case of the type attribute exists for each "to-many" relationship.
As previously mentioned, the two java.util.Vector s in
Product handle the one-to-many and many-to-many
relationships. The one-to-many relationship exists in the
<field> element called details. The information
in the details element tells Castor that the property is a
Collection of type java.util.Vector ; that the
Collection contains objects of type
ProductDetail ; and that it is required (that is,
cannot be null). The <sql> element tells Castor that
the <field> 's object mapping contains an SQL column
called prod_id and that this column should be used to identify the
one-to-many relationship.
The many-to-many relationship exists in the <field>
element called categories. The categories element tells Castor that
the property is a Collection of type
java.util.Vector ; that the Collection contains
objects of type Category ; and that it is required
(that is, it cannot be null). The <sql> element tells
Castor that this relationship makes use of an additional table called
category_prod. It also states that this
<field> 's object mapping contains an SQL column called
prod_id, and that this column should be used along with
category_id to identify the many-to-many relationship.
One more relationship exists between ProductGroup and
Product . This relationship is a one-to-many relationship
whereby many Product s can have a relationship to the same
ProductGroup . While this relationship is the same as the
one-to-many relationship between Product and
ProductDetail , it operates in reverse (many-to-one), so we
only see the "to-one" side of the relationship in the mapping.
Inheritance in
Castor Castor makes use of two types of inheritance: Java
inheritance and relational inheritance. Listing 3 contains the mapping for
Computer . As you may recall from Figure
1, Computer is an extension of Product .
When a Java object simply extends a generic base class or implements an
interface, the inheritance need not be reflected in the mapping
descriptor. Because both Product and Computer
are mapped classes, however, the inheritance must be noted for Castor to
reflect it properly. The extends attribute of the
<class> element is used to represent the relationship
between Computer and Product . Note in Figure
1 that the comp (computer) table does not contain the columns from the
prod (product) table. Rather, the prod table contains the base information
and the comp table extends it. Listing 3. The mapping
for Computer
<class name="myapp.Computer" extends="myapp.Product" identity="id">
<description>Computer definition, extends generic
product</description>
<map-to table="computer" xml="computer" />
<field name="id" type="integer">
<sql name="id" type="integer" />
</field>
<field name="cpu" type="string">
<sql name="cpu" type="char"/>
</field>
</class>
|
Dependent versus related
relationships Castor distinguishes the relationship of two
objects as either dependent or related, and maintains
different life cycles for each of the two types of relationship. Up to
this point, we've only discussed independent (or related) objects.
Independent objects are those objects that do not have the depends
attribute specified in the <class> element of the
mapping descriptor. If you want to perform any CRUD (create, read, update,
delete) operation on an independent object, you can do so directly on the
object.
Listing 4 contains the mapping for the dependent object
ProductDetail . Notice that the <class> element contains
the depends attribute. This tells Castor that
ProductDetail is dependent upon Product for all
operations. In this case, Product is the master object and
ProductDetail is the dependent object. If you want to perform
any CRUD operations on the ProductDetail object, you can do
so only through its master object. That is, you must first fetch a
Product and navigate to ProductDetail using the
accessor method. Each dependent object may have only one master
object. Listing 4. The mapping for
ProductDetail
<class name="myapp.ProductDetail" identity="id" depends="myapp.Product">
<description>Product detail</description>
<map-to table="prod_detail" xml="detail" />
<field name="id" type="integer">
<sql name="id" type="integer"/>
</field>
<field name="product" type="myapp.Product">
<sql name="prod_id" />
</field>
<field name="name" type="string">
<sql name="name" type="char"/>
</field>
</class>
|
Performing queries on the object
model Castor provides an implementation of a subset of the
ODMG 3.0 specification for the Object Query Language (OQL). The syntax for
OQL is similar to that of SQL but it lets you query the object model
rather than directly querying the database. This can be a powerful asset
when you're supporting more than one database. Internally, Castor's OQL
implementation translates the OQL query into the appropriate SQL for the
database. Parameters are bound to a query using the bind()
method. Below are some simple examples of OQL queries.
Rather than continue to use a fully qualified object name throughout a
query, Castor's OQL implementation supports the use of aliases for
objects. In the queries below, c is one such alias.
If you wanted to query for all Computer s, you would
execute the following query:
SELECT c FROM myapp.Computer c
|
If you wanted to query for a Computer whose ID equaled
1234, you would start with:
SELECT c FROM myapp.Computer c WHERE c.id= $1
|
followed by:
To query for a Computer whose name is like a particular
string, you would execute the following query:
SELECT c FROM myapp.Computer c WHERE c.name LIKE $1
|
followed by:
To query for a Computer whose ID fell within a list of
IDs, you would execute the following query:
SELECT c FROM myapp.Computer c WHERE c.id IN LIST ( $1, $2, $3 )
|
followed by:
query.bind( 97 )
query.bind( 11 )
query.bind( 7 )
|
Further details on the Castor OQL implementation are beyond the scope
of this article. To learn more, see the references in the Resources
section.
Transactions in
Castor Castor's persistence operations take place within the
context of a transaction. However, Castor is not a transaction manager.
Rather, it is much more of a cache manager, in that it utilizes the
atomicity of transactions to persist objects to a database. This setup
allows the application to more easily commit or rollback any changes to an
object graph. An application running in a non-managed environment must
explicitly commit or rollback transactions. When running in a managed
environment, the application server can manage the transactional
contexts.
Short transactions versus long
transactions Normal transactions in Castor are referred to
as short transactions. Castor also provides the notion of long
transactions, which are made up of two short transactions. We can use
a typical Web application to understand the difference between the two
types of transaction. In the case of an application that calls for data to
be read from a database, displayed to a user, and then committed to the
database, we're looking at a potentially lengthy transaction time (which
is typical for Web applications). But a write lock in the database can't
be held indefinitely. To work around this, Castor utilizes two short
transactions. For example, the first short transaction materializes
objects using a read-only query, which lets you display the objects
without leaving the transaction open. If the user makes changes, a second
short transaction is started and the update() method brings
the changed objects into the transaction's context. Listing 5 is an
example of a long transaction. Listing 5. A long
transaction example
public LineItem getLineItem()
{
db.begin();
LineItem lineItem = ( LineItem ) db.load( LineItem.class,
lineItemNum, Database.ReadOnly );
db.commit();
return lineItem;
}
public void updateOrder( LineItem li )
{
db.begin();
db.update( li );
db.commit();
}
|
Because the time interval between the first short transaction and the
second short transaction is arbitrary, you'll have to perform a dirty
check to determine whether the object has been changed in the database.
Dirty checking is used to verify that an object has not been modified
during a long transaction. You can enable dirty checking by specifying the
dirty attribute in the <sql> element of the
mapping descriptor. For this to work properly, each object must hold a
timestamp. If you implement the Timestampable callback
interface, Castor will set the timestamp in the first short transaction
and check it during the second short transaction.
Database support
The main
feature of Castor JDO is that it provides an API for binding Java
objects to relational databases. Castor JDO supports the following
databases:
- DB2
- HypersonicSQL
- Informix
- InstantDB
- Interbase
- MySQL
- Oracle
- PostgreSQL
- SAP DB
- SQL Server
- Sybase
- Generic (for generic JDBC support)
Adding support for additional databases is fairly easy. See Resources
for further information. |
Conclusion In this
article, we've discussed the basics of mapping a relational data model to
a Java object model using Castor. The examples used in this article
closely follow the examples currently provided with the Castor source
code. By no means do these simple examples cover the breadth of Castor's
capabilities. In fact, Castor supports many other features, including key
generation, lazy loading, an LRU cache, different locking/access modes,
and much more. If you want to learn more about these features and many
others, consult the Resources
section.
Castor JDO is just one of many data-binding solutions available in the
open source world today. As this article has shown, Castor provides a
sophisticated alternative to in-house solutions, which generally require
you to maintain different SQL and JDBC code for each database you support.
Furthermore, because it is an open source, community-based project, Castor
is constantly improving. I encourage you to check out the technology, and
maybe even become a part of the Castor community.
Resources
About the
author Bruce Snyder lives in the Denver, Colorado,
metropolitan area, where he has worked at various startups
implementing J2EE and related solutions over the last few years.
Currently, Bruce is a senior software engineer at DigitalGlobe, a
satellite imagery and information company located in Longmont,
Colorado. Bruce's experience with Java technology and J2EE runs the
gamut from system design and architecture to implementation and
testing. After working with Castor and object-relational data
binding for nearly two years, Bruce recently became lead developer
of the Castor JDO team. You can contact him at ferret@frii.com.
|
|