Java Data Object (JDO) is a standard API generically used to store,
retrieve, and query user-written object classes to and from a data store.
What makes JDO stand out among other persistence options is that it's
easy to use and flexible.
JDO provides transparent persistence so it's easy for developers to
persist objects without doing any extra work. The tedious and routine
persistence work is offloaded to the JDO vendor, leaving developers with
time to concentrate on the business logic. In addition, JDO is flexible
because it can work on any data store. While JDBC provides persistence only
for relational databases, JDO is more generic, providing persistence for any
data store, for example, relational databases, files, XML and object
databases, etc., making applications very portable.
Overview
Metadata and Enhancer
In JDO, any object class to be persisted needs to be PersistenceCapable,
while any class that references a persistence-capable object needs to be
PersistenceAware. The good news is that with JDO transparent persistence,
you don't have to program your class to implement PersistenceCapable or
PersistenceAware. Just write your object class as usual, and the JDO vendor
implementation will provide an Enhancer that will make your object class
PersistenceCapable based on the metadata you provide. The only extra work
you need to do is to write a metadata file in XML for your object. Listing 1 shows the metadata that I'll be using later in my code example.
The metadata is usually short and not hard to write because by default,
JDO already derives a lot of information from the object class. Specify
information in the metadata only when:
You need to override JDO default behavior, e.g., making a field
nonpersistent even though it's not transient.
There's information JDO can't derive from the class definition, e.g.,
which field is the primary key or what kind of object is inside a
Collection.
PersistenceManager
With object classes enhanced, you can then persist the object by using a
PersistenceManager. To get a PersistenceManager, first specify a Property object, which usually contains:
- Data store connectivity information
- JDO vendor class name
- Default settings for the PersistenceManager
Lines 23 in Listing 3 show how you can get a PersistenceManager from a JDOPersistenceManagerFactory by specifying a Property object. Once you have
a PersistenceManager, you can use it to add, update, delete, and query
objects (which I'll discuss in the next section). When you're done, close
the PersistenceManager to free up its resources at the end.
Listing 3 shows code fragments of how you can persist and query objects
using JDO. With a PersistenceManager pm, you can add a new object to a data
store by makePersistent (Lines 68). An object only needs to be made
persistent the first time it's known; once an object is already
makePersistent, you can update the object directly by referencing its
fields. All changes to that object will be saved to persistence storage when
the transaction commits. If you don't like those changes, roll back to undo
them (Lines 1517). Similarly, you can delete an object with
deletePersistent (Line 26).
To access objects in the data store, iterate through an Extent, which is
the logical representation of all persistent instances of a given
persistence-capable class (Lines 1215).
However, if you want to be more selective and get only a subset of
instances of a given class, create a query. To do this, pass a Candidate
object and a Filter to the method newQuery. The Candidate object is a set of
objects to select an instance from; it can be a collection of objects or an
extent. The filter is a string written in JDO Query Language (JDOQL). Once
you create the query, execute it and get back a collection of instances that
match (Lines 2226). JDOQL is the query language for JDO; it's somewhat
similar to SQL but has a Java syntax. The example here is a simple one; with
JDOQL, your Filter string can be much more sophisticated. In addition, if
you declare parameters to act as placeholders in the filter string, you can
write a single query and then execute it multiple times, supplying new
values each time. There's a lot more to JDOQL that you can read up on;
please refer to the links in the Resources section.
A unique thing about JDO's query capability is that once you get an
object, you can navigate to any other object referenced by that object.
Frequently, all you need is to get a starting point object, and you can
already get to any object related to it without having to run another query.
An Object Persistence Example
To find out if JDO is as good as promised, I'll write some code using
JDO and JDBC to persist a Book object that I created (see Listing 4). This Book object contains a name and a Block object. To make things interesting,
a Book has a constraint that each book is uniquely defined by its name,
meaning that you're not allowed to add two books with the same name.
A Block is a building block for a Book. It can be of type Document,
Chapter, or Section. The root Block is of type Document and can contain any
number of Chapter Blocks. Each Chapter Block can also contain any number of
Section Blocks, so there's a recursive relationship here for Blocks. Within
each Block, there's a HashMap that may store any number of attribute value
pairs for that Block.
Listing 5 shows a test Book I created for my example. This Book contains two chapters: Chapter 1 has one section and Chapter 2, two. In particular,
Chapter 2 has an attribute Color=Red.
Using this Book class, I want to implement some common persistence
features as follows:
Add: See if I can successfully add two books to a data store, and that when I add a third book with the same name, an exception is thrown because the integrity constraint is violated for the unique book name.
Update: See if I can update a Book by adding an attribute "Comment" to its root Block. When I commit, the change should be saved, and if I roll back, the change should be discarded.
Delete: See if I can look up a book by a query and delete it from the data store.
Without JDO, my usual way to implement this would be to design
relational database tables to store all the data contained in a Book, then
use SQL and JDBC to store/retrieve that data to/from the tables. Due to
space constraints, I won't show you my JDBC implementation here, but you can
download it from below if you're interested. Note that to
implement the above features in JDBC/SQL, I have to write quite a lot of
code (480 lines!). What I'm going to show you now is a much shorter way to
address the same problem using JDO.
Persisting a Book Object Using JDO
To persist a Book object in JDO, although I'm using exactly the same
object class as I would use if I implemented this in JDBC, the ID field in
the Block object is now unnecessary. If I implement this in JDBC, the ID
field would be needed to internally reference a different Block from the
database table. However, using JDO, I don't have to worry about populating
this field since JDO will handle that internally.
To persist my Book object, I create metadata for both a Book and a Block
(see Listing 1). In the metadata for Block, I specify that the object in the
Collection children is of type Block (Lines 1011), while the key and value
for HashMap attributes are of type String (Lines 1214). Also, since the ID
field is not really needed for the Block object, I specify in the metadata
that there's no need to persist it (Line 8). In the metadata for Book, I
specify that nm is the primary key of Book (Line 5), and that the Book
object should use my user-defined Application Identity BookKey as the object
identity class (Line 4). The code for the class BookKey can be found in
Listing 6.
In this example, the JDO Vendor implementation I use is Kodo JDO (which
uses a relational database). There are many JDO implementations in the
market; you can choose to use any of them, and your application code doesn't
need to be changed. For the data store, I use Enhydra InstantDB (a
relational database included with the Kodo distribution). The essence of JDO
is that the developer does not need to know how the vendors persist data to
a database, so I don't need to design any table here even though we are
using a relational database behind the scenes. The vendor Kodo provides a
tool called schematool to help me create those tables based on my metadata.
All I need to do now is run the following to prepare the database for my
object.
schematool action refresh Book
schematool action refresh Block
Next I compile my object classes as usual, then using Kodo's enhancer
tool jdoc, I enhance my class file by running:
jdoc.Book
jdoc.Block
jdoc.BookPersistJDO
Here, as long as I put the unenhanced class files (Book.class,
BlockPersist.class, BookPersistJDO.class) and the metadata file in a
location where jdoc can find it, jdoc will then modify the bytecode of these
classes to add methods necessary to make them PersistenceCapable or
PersistenceAware. Classes with metadata will be enhanced to
PersistenceCapable, while classes without metadata will be enhanced to
PersistenceAware. In this example, Book.class and Block.class are enhanced
to be PersistenceCapable while BookPersistJDO.class is enhanced to be
PersistenceAware.
Once I've enhanced my code, any persistence work for my object can be
done via the PersistenceManager. Using the code I showed you previously, I
can easily get a PersistenceManager pm and then add, delete, or update a
book using it. Listing 7 shows fragments of my code BookPersistJDO.java. The method addBook (Line 3) shows how I add a Book in JDO, and the method deleteBook (Line 13) shows how I delete a book.
To get a Book with a given name, I create a query using a Filter written
with JDOQL. Executing the query, I get back a Collection of Book object that
matches this query (Lines 2529). Once I get a Book object, I directly
update its field and then commit the changes.
Figure 3 shows the results of running my test on this implementation.
The results are as I expected. First, I successfully added books to the data
store, and when I add a book with the same name, it gives me a
JDOUserException, showing that the unique book name integrity is violated.
Next, I'm able to update a book, then keep the changes by committing, or
discard the changes by rolling back. Last, I'm able to query the data store
to get a book by its name, and then delete the book from the data store.
Figure 3
Comparison of Implementations
By using JDO and JDBC to tackle the same problem of persisting a Book
object, I observed the following:
1. Using JDO, I'm able to achieve the same things I can achieve with
JDBC. I can query objects using JDOQL; maintain data integrity by specifying
the nm of a Book as a unique primary key; and add, delete, and update my
object.
2. JDO makes my transaction handling easier. In my JDBC implementation,
because one Book object actually translates to many rows in four different
relational tables, I have to ensure that all inserts or deletes to tables
are done in a transaction. On the contrary, JDO saves or deletes the whole
object to the database in one operation, and I don't need to use
transactions to maintain the atomicity of the action.
3. The fact that BookPersistJDO.java (140 lines) is so much shorter than
BookPersistJDBC.java (480 lines) shows that JDO simplifies my code a lot.
This is especially so given the recursive nature of my object and how
complicated it is to represent it in relational tables. In my JDBC
implementation, I have to put a lot of thought into the design of my tables
so I can store and retrieve the recursive data. I have to generate an ID for
each Block to use as a link for the child/parent relationship. In the JDO
implementation, I don't have to think about any of that, and everything is
already persisted correctly.
4. The effort I put into maintaining a cache for performance in my JDBC
implementation is not needed in JDO, because the JDO vendors are the ones
who would implement data caching for performance improvement. That saves me
a lot of work because I don't have to worry about keeping my cache in sync
with the database all the time.
Behind the scenes, the JDO vendor's implementation and my JDBC
implementation are probably not much different. For example, this JDO
vendor, using a relational database, may also have implemented it with a
similar table design and ID generation scheme, and used JDBC to persist the
data. However, the important point is I don't have to know about these
implementation details: they're all offloaded to the JDO vendor who has
expertise in that area, and who would likely implement it better. In
addition, the vendor is also free to implement it with any other kind of
data store like object databases and files, giving us more flexibility and
choices on where to store the data.
Conclusion
JDO offers a lot of advantages for developers:
It has all the basic functionalities needed for data persistence: add,
delete, update, transaction, data integrity, and data caching.
It takes over a lot of tedious work from the developer, making code
easy and maintainable.
It's vendor independent, preventing vendor lock in.
Although my example doesn't show it, it can work on any data store,
making development flexible and portable.
JDO is a technology that's worth exploring. This article is a starting
point for you; for more information see the resources section.
Resources
The One-Stop Site for JDO:
www.jdocentral.com
The Specification:
http://access1.sun.com/jdo/
Roos, R. (2002). Java Data Objects. Addison Wesley:
www.OgilviePartners.com
The JDO Vendor I used in my example: SolarMetric:
www.solarmetric.com/Software/Kodo_JDO
Properties for JDO
Listing 2 provides the Property file that I'll use later. I'm using a relational data store and Lines 14 contain JDBC connectivity information.
Line 6 contains the name of the JDO vendor class I'm using. Lines 810 are
the default settings I want for the PersistenceManager. It specifies that
I'm using optimistic transaction, and that values in the cache should be
restored on a transaction rollback, and should not be retained on a commit.
There are many more details on how you can use different options for
transactions; read the JDO API to find out what they all mean and when to
use them.
JDBC Implementation
Here I'll briefly describe how I implement the persistence of Book using
JDBC. By knowing how much work is involved in this, you can better
appreciate how much JDO does for you.
To persist Book to a relational database, I created four tables (see
Figure 1). Based on data integrity, the nm field in the Book table is
defined as a unique key. To trace the relation between each Block to its
children and attributes, I assign a blockId to each Block. This blockId is generated (by
incrementing 1 to the largest blockId in the Block table) every time a new
Block is added.
Figure 1
Adding a Book object involves denormalizing the information within a
Book object into four tables: Book, Block, BlockRelation, and BlockAttr. In
particular, a blockId has to be generated for each Block. Furthermore, the
tedious part is that Blocks are recursive, and I have to write code that
recursively inserts the Block data.
For the example Book created in Listing 5, the tables will be populated
as shown in Figure 2. As you can see , one Book is translated into many rows
in four tables, so all inserts have to be wrapped in a transaction so that a
Book is added all or none, never partially. Similarly, deleting a Book
involves finding the appropriate rows to delete in each of the four tables,
while wrapping all deletes in a transaction so that a Book is deleted all or
none.
Figure 2
For better performance, I don't want to query four tables to recursively
generate the Book object every time someone asks for a Book. Instead I
created a BookCache that I populated at the beginning by generating all Book
and Block objects from the four tables. As I add, delete, and update Books
in the database, my code ensures that the BookCache is kept in sync. As a
result of all this work done in maintaining a cache, to get a Book is as
easy as looking up the cache by its name.
Not Yet Perfect
Despite all its advantages, there are still areas where JDO may not be
perfect yet:
It's good for new development, but to convert existing schema in a
relational database to use JDO requires a bit of mapping work.
As developers, we no longer deal with lower-level database access when
using JDO, so it may be hard for us to tune performance. Since JDO
implementation has to do a lot of extra work tracking the fields that are
changed or synchronizing cache internally, etc., how well the JDO vendor
implements it will be critical to performance.
JDOQL does not have an aggregate function such as max, min, and sum
like SQL does.
It would be nice if there were better checking to catch bad JDOQL at
compile time. For example, in a Filter, when you specify the field name in
the class via a string, you can easily have specified a bad field name
resulting in a JDOQL that will compile, but fail at execute time.
Some people thought that JDO's advantage is that you don't have to
write SQL anymore; the truth is now you have to learn to write JDOQL
instead!
Author Bio
Teresa Lau has been an independent Java consultant for over five years, with
an emphasis on financial systems. She holds an MS in computer science and
currently works in New York.
ttylau@yahoo.com
"Java Data Object"
Vol. 8, Issue 3, p. 36
Listing 1 Metadata for my example
1 <?xml version="1.0"?>
2 <jdo>
3 <package name="whs.jdo">
4 <class name="Book"
identity-type="application"
objectid-class="BookKey">
5 <field name="nm"
primary-key="true" />
6 </class>
7
8 <class name="Block" >
9 <field name="id"
persistence-modifier="none" />
10 <field name="children">
11 <collection
element-type="Block"/>
12 </field>
13 <field name="attributes">
14 <map key-type="String"/>
15 <map value-type="String"/>
16 </field>
17 </class>
18 </package>
19 </jdo>
Listing 2 Properties for setting up JDO
1 javax.jdo.option.ConnectionUserName=
Database user
2 javax.jdo.option.ConnectionPassword=
Password of the user
3 javax.jdo.option.ConnectionURL=
URL of the database
4 javax.jdo.option.ConnectionDriverName=
Classname of JDBC driver
5
6 javax.jdo.PersistenceManagerFactoryClass=
com.solarmetric.kodo.impl.jdbc.
JDBCPersistenceManagerFactory
7
8 javax.jdo.option.Optimistic=true
9 javax.jdo.option.RestoreValues=true
10 javax.jdo.option.RetainValues=false
Listing 3 JDO Code Fragment
1 // --- Get persistence manager ---
2 PersistenceManagerFactory factory =
JDOHelper.getPersistenceManagerFactory
(property);
3 PersistenceManager pm =
factory.getPersistenceManager()
4
5 // --- Add ---
6 pm.currentTransaction().begin ();
7 pm.makePersistent (obj);
8 pm.currentTransaction().commit ();
9
10 // --- Iterate Extent & Update ---
11 pm.currentTransaction().begin ();
12 Extent ext = pm.getExtent
(MyClass.class, false);
13 for (Iterator i = ext.iterator ();
i.hasNext();) {
14 MyClass obj = (MyClass)i.next ();
15 obj.setField1("AA");
16 }
17 pm.currentTransaction().commit ();
18
19
20 // --- Query and Delete ---
21 pm.currentTransaction().begin ();
22 String filter ="nm=\"JDO Book\"";
23 Query qry= pm.newQuery(ext, filter);
24 Collection c =
(Collection) qry.execute();
25 Object obj = c.iterator().next();
26 pm.deletePersistent (obj);
27 pm.currentTransaction().commit();
28
39 // --- Close resources ---
30 pm.close();
Listing 4 Book & Block object
1 class Book {
2 String nm;
3 Block block= new Block("Document");
4
5 Book(String name){
6 this.nm = name;
7 }
8
9 void addChild(Block e) {
10 block.addChild(e);
11 }
12 }
13
14 class Block {
15 String type;
16 Integer id;
17 Map attributes = new HashMap();
18 List children= new ArrayList();
19
20 Block(String type) {
21 this.type = type;
22 }
23
24 void addChild(Block e) {
25 children.add(e);
26 }
27
28 void setAttribute(String key,
String attr){
29 attributes.put(key, attr);
30 }
31 }
Listing 5 Create Test Book
1 Book book = new Book("Intro to JDO");
2 Block chp1= new Block
("Overview", "Chapter");
3 Block sec11 = new Block
("Advantage of JDO", "Section");
4 chp1.addChild(sec11);
5 Block chp2 = new Block
("Example", "Chapter");
6 chp2.setAttribute("Color", "Red");
7 Block sec21 =new Block
("JDBC Code", "Section");
8 Block sec22 = new Block
("JDO Code", "Section");
9 chp2.addChild(sec21);
10 chp2.addChild(sec22);
11 book.addChild(chp1);
12 book.addChild(chp2);
Listing 6 BookKey.java
1 public final class BookKey {
2 public String nm = null;
3 public BookKey(){}
4
5 public BookKey(String nm) {
6 this.nm = nm;
7 }
8
9 public boolean equals (Object o){
10 if (o == this) return true;
11 if (!(o instanceof BookKey))
return false;
12 return((BookKey)o).nm.equals(nm);
13 }
14
15 public int hashCode () {
16 return nm.hashCode();
17 }
18
19 public String toString() {
20 return nm;
21 }
22 }
Listing 7 BookPersistJDO.java
1 class BookPersistJDO {
2
3 public Book addBook (Book book)
4 throws JDOUserException {
5 PersistenceManager pm =getPM();
6 Transaction tran =
pm.currentTransaction ();
7 tran.begin ();
8 pm.makePersistent (book);
9 tran.commit ();
10 return book;
11 }
12
13 public void deleteBook (Book book){
14 PersistenceManager pm =getPM();
15 Transaction tran =
pm.currentTransaction();
16 tran.begin ();
17 pm.deletePersistent (book);
18 tran.commit ();
19 }
14
15 public Collection getAllBooks() {
15 PersistenceManager pm = getPM ();
17 String filter ="";
18 Extent extent =pm.getExtent
(Book.class, false);
19 Query qry = pm.newQuery
(extent, filter);
20 return (Collection)qry.execute();
21 }
22
23 public Book getBook(String name) {
24 PersistenceManager pm = getPM ();
25 String filter="nm==\"" +
name +"\"";
26 Extent extent = pm.getExtent
(Book.class, false);
27 Query qry = pm.newQuery
(extent, filter);
28 Collection c =
(Collection) qry.execute ();
29 return (Book) c.iterator().next();
30 }
31 }
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail: info@sys-con.com
Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.
|