Java Data Objects - An Introduction

by Thomas Paul

The Story Begins

Java Data Objects (JDO) is a specification designed to provide a standardized way to make objects persistent in a database. JDO allows you to treat rows in your database as if they were Java objects. This is similar to other "object-relational mapping" implementations such as Hibernate and Jakarta OJB, however JDO is designed to do what these products do in a standard way.

From an application developers point of view JDO serves the same role as JDBC but it has two main advantages over writing programs using JDBC:

  1. It simplifies accessing the underlying datastore by isolating business application developers from the intricacies of JDBC.
  2. JDO implementations can keep a global cache of objects. Calls to the physical database can be eliminated by using objects that are already in the JDO cache.

Coding JDO

There are actually two parts of mapping objects to rows in a database. The first part is the code that you write in your programs to create objects, get objects, update objects, and delete objects. The second part is the actual mapping of these objects to an actual database. JDO only provides implementation details for the first part. The second part is database specific and how you do the actual mapping will vary from implementation to implementation. JDO is a specification so "out-of-the-box" it doesn't actually support any database. The JDO reference implementation (JDORI), which we will take a look at, supports a pseudo-database that doesn't show how to implement with a real database but it will at least give us an idea of how to write code that uses JDO.

JDO uses a datastore, which represents whatever is the source of the data that JDO will be using. This could be a relational or object database, for example. To access the datastore, JDO uses a class which implements the PersistenceManager interface. This class will be vendor specific. A class that implements the PersistenceManagerFactory interface is used to get the correct PersistenceManager for the datastore that you are accessing. Getting the correct PersistenceManagerFactory is dependent on properties that are supplied to the JDOHelper class.

An example of the JDO properties (used in the example code):

javax.jdo.PersistenceManagerFactoryClass=com.sun.jdori.fostore.FOStorePMF
javax.jdo.option.ConnectionURL=fostore:database/fostoredb
javax.jdo.option.ConnectionUserName=root

The example above specifies that the JDOHelper should find a PersistenceManagerFactory named com.sun.jdori.fostore.FOStorePMF, that it will access a datastore named fostoredb, which is found in the database directory, and that it should connect with the user id of root.

The properties are passed to the getPersistenceManagerFactory( ) static method of the JDOHelper class in the form of a Properties object. An example:

PersistenceManagerFactory pmf =
               JDOHelper.getPersistenceManagerFactory(properties);

A PersistenceManager can then be obtained from the PersistenceManagerFactory. An example:

PersistenceManager pm = pmf.getPersistenceManager();

JDO uses an implementation of the Transaction interface to control transaction flow for all of its functions so you will need to obtain a Transaction object in order to use JDO. A Transaction object can be created using the PersistenceManager.

Transaction tx = pm.currentTransaction();

Configuring JDORI

Before we continue, let's set up an environment that we can use to test some of these JDO commands. You will need to download the JDO reference implementation (JDORI).

The download contains three jar files that you will need to add to your classpath:

In addition to these you will also need three other jar files to use the JDORI:

In order to create the datastore used in the examples, you will need to write a simple program that creates the datastore. If you are using a real implementation then you would not do this step and the properties you use to access the datastore will be different. Assuming that you have stored the properties listed above in a file called datastore.properties, your program would look something like this:

import java.io.*;
import java.util.*;
import javax.jdo.*;

public class CreateDataStore {
    public static void main(String[] args) {
        try {
            Properties properties = new Properties();
            InputStream stream = new FileInputStream("datastore.properties");
            properties.load(stream);
            properties.put("com.sun.jdori.option.ConnectionCreate", "true");            
            PersistenceManagerFactory pmf =
                    JDOHelper.getPersistenceManagerFactory(properties);
            PersistenceManager pm = pmf.getPersistenceManager();
            Transaction tx = pm.currentTransaction();
            tx.begin();
            tx.commit();
        } catch (Exception e) {
            System.out.println("Problem creating database");
            e.printStackTrace();
        }
    }
}

Notice that we are importing the package javax.jdo. All programs that use JDO will import this package. Notice that in addition to the properties listed above we added one more entry to the Properties object that causes the datastore to be created. As per the javax.jdo.option.ConnectionURL entry in the datastore.properties file, the datastore will be created in a directory called datastore and will be given the name fostoredb.

Creating a Class to Persist

Now that we have a datastore, we need to create a class that can be stored in this datastore. This class will represent a row in a table in a database. For our example, we will create a class called Book that will represent rows in a table called Book. The code for the Book class is similar to many simple JavaBean classes. The class must have a default constructor and it must have get and set methods for each of the instance variables of the class. Here is our sample class:

package com.javaranch.jdotest;

public class Book {
    private String title;
    private String author;
    private String isbn;
    private float price;
    
    public Book() {}
    public Book(String title, String author, String isbn, float price) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.price = price;
    }
    
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public String getIsbn() { return isbn; }
    public float getPrice() { return price; }
    
    public void setTitle(String title) { this.title = title; }
    public void setAuthor(String author) { this.author = author; }
    public void setIsbn(String isbn) { this.isbn = isbn; }
    public void setPrice(float price) { this.price = price; }
}

Before we can use this class we need to run the class file through a JDO enhancer. The enhancer will add methods and variables to the class in order to make it manageable by the JDO implementation. Running through the enhancer can be done from the command line or as part of an ANT script. I will show the commands for each method.

Before you can enhance the class file you will need to create an XML file that is used by the enhancer. This file identifies the classes that you will use and also allows you to specify special field requirements if required. The file should be stored in the same directory as the class file and should be named package.jdo. For our example, the file would look like this:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE jdo PUBLIC "-//Sun Microsystems, Inc.//DTD Java Data Objects Metadata 1.0//EN" 
        "http://java.sun.com/dtd/jdo_1_0.dtd">
<jdo>
    <package name="com.javaranch.jdotest" >
        <class name="Book" />
    </package>
</jdo>

An entry must be placed in this file for each class you wish to enhance. Once you have created this file and compiled your source, you are ready to start the enhancement. Enhancement in JDORI is done using a class named com.sun.jdori.enhancer.Main. The enhancer in another implementation will be different (or may not be required).

From the command line, you would do the following:

java com.sun.jdori.enhancer.Main -s classes 
              classes/com/javaranch/jdotest/Book.class

This assumes that we have stored our class file in a directory called classes. The result will be that the class file has been overwritten with an enhanced version.

To do the same thing from an ANT script we would add an ANT task:

<!-- ===================================== -->
<!-- Enhance                               -->
<!-- ===================================== -->
<target name="enhance" depends="compile" >
    <java fork="true" classname="com.sun.jdori.enhancer.Main">        
        <arg line="-s classes classes/com/javaranch/jdotest/Book.class" />
    </java>
</target>

I have found that you need to run the task with fork="true" because of a problem with Xerces that causes the task to fail otherwise.

Persisting Objects

Now that we have a datastore and an enhanced class to place in our datastore, we can write some code to get our PersistenceManager and create a Transaction object. Here is the code:

InputStream stream = new FileInputStream("datastore.properties");
Properties properties = new Properties();
properties.load(stream);
PersistenceManagerFactory pmf =
              JDOHelper.getPersistenceManagerFactory(properties);
PersistenceManager pm = pmf.getPersistenceManager();
Transaction tx = pm.currentTransaction();

This code will be the same for any JDO implementation. The only difference will be the properties used to create the PersistenceManagerFactory. We can now look at some examples of using JDO.

First, let's create a Book object and add it to our datastore:

Book book = new Book("Benjamin Franklin : An American Life", 
       "Walter Isaacson", "0684807610", 18.00f);
tx.begin();
pm.makePersistent(book);
tx.commit();

The makePersistent( ) method will cause the Book object to be stored in our datastore.

Suppose we want to fetch this object from the datastore:

tx.begin();
Query q = pm.newQuery(Book.class, "isbn == \"0684807610\"");
Collection result = (Collection) q.execute();
Book book = (Book)result.iterator().next();
q.close(result);

A Query object is used to select objects from the datastore. Creating the Query object requires that we pass a Class object representing the table we wish to select from and a filter representing the criteria used to select objects from the datastore. JDO Query Language (JDOQL) is used to create filters.

Notice that we did not commit the transaction. This was deliberate. Since we have the object, we can make changes to the object and when we commit, those changes will be persisted to the database. For example, suppose we want to update the title of this book:

book.setTitle("Paul Wheaton : An American Life");
tx.commit();

This change will be automatically written out to our datastore. Deleting our book is also simple. While we have an active transaction we can simply do this:

pm.deletePersistent(book);
tx.commit();

In addition to the commit( ) method of the Transaction object, there is also a rollback( ) method that can be used to cancel any transaction.

Conclusions

JDO has several limitations that keep it from being a replacement for JDBC in all cases. The query language is implementation specific so it may not be as robust as SQL. DDL may or may not be supported. Many of the best features of JDO such as caching are also vendor specific and may or may not be available in the JDO implementation that you use. Also, JDO is still a new technology and has not been nearly as well adopted as JDBC. Will JDO take the Java world by storm? So far the verdict is still out but it is certainly a technology to keep your eye on.