Getting Started with JDO
"The expert at anything was once a beginner."
Hayes
Using JDO to build an application that creates, reads, updates, and deletes persistent instances of Java classes is easy and requires only some basic knowledge about how JDO works and how to use it. Armed with this knowledge, you can develop your first JDO application and persist instances of Java classes transparently in a datastore. This chapter is a guide to getting started with JDO, providing an understanding of how JDO works and how to use the basic APIs, and exploring some of the more advanced concepts related to using JDO.
This chapter covers these topics:
-
How JDO is able to transparently persist instances of Java classes.
-
The basic JDO interfaces and how they are related.
-
How to define a Java class that can be used with a JDO implementation.
-
How to connect to a datastore.
-
How to create, read, update, and delete persistent objects.
-
The types of fields, system classes, collection classes, and inheritance supported by JDO.
-
How to handle exceptions within an application.
-
The concept of object identity.
-
The different types of identity that can be used.
-
How concurrency control is enforced between multiple applications.
The examples for this chapter can be downloaded from the Internet at www.corejdo.com and are located in the com.corejdo.examples.chapter3 package. In many cases, the code snippets shown are simplified versions of the actual classes to allow the examples to focus only on the relevant concepts.
3.1 How Does JDO Work?
The goal of the JDO is to allow a Java application to transparently store instances of any user-defined Java class in a datastore and retrieve them again, with as few limitations as possible. This book refers to the instances that JDO stores and retrieves as persistent objects. From the application perspective, these persistent objects appear as regular, in-memory Java objects. However, the fields of these instances are actually stored in some underlying datastore persistentlyall without any explicit action on behalf of the application.
JDO has nothing to do with where methods are executed; it does not provide a means of remote method invocation à la RMI and EJB, nor does it store and execute methods in some datastore. JDO simply specifies how the fields of a persistent object should be managed in-memory, being transparently stored to and retrieved from an underlying datastore. With JDO, methods are invoked on a persistent object by an application as per any regular in-memory Java object. Figure 3-1 provides a schematic of how JDO works.
Figure 3-1. JDO runtime environment.
The JDO implementation and the application run together in the same JVM. The application delegates to the JDO implementation to retrieve the fields of persistent objects as needed. The JDO implementation tracks modifications to the fields and writes these changes back to the datastore at the end of the transaction. The JDO implementation is responsible for mapping the fields of the persistent objects to and from memory and the underlying datastore.
JDO achieves transparency of access by defining a contract to which a class must adhere. Any class that implements this contract can then be used with any JDO implementation. JDO requires that a JDO implementation ensure that any class that adheres to the JDO persistence-capable contract can be used with any JDO implementation, without recompilation.
The ability to run a JDO application with any JDO implementation is akin to using JDBC, a JDBC application can be run "as is" using JDBC drivers from different vendors and even using different relational databases. In fact, JDO is somewhat better than this, because with JDBC an application is still prone to differences in SQL support across different databases. With JDO, SQL is not directly exposed. Although a JDO runtime may itself use JDBC to access a relational database as its datastore, it is the responsibility of the JDO implementation to resolve the differences in SQL support across databases.
Even better, unlike SQL, a JDO application can work "as is" across different types of databases, not just relational: object databases, flat-files, and so on. All that is required is a JDO implementation that supports the datastore.
The JDO specification defines the persistence-capable contract as a Java interface, called PersistenceCapable, and a programming style that the class implementation must follow. A class that adheres to this contract is referred to as being "persistence-capable."
A class is said to be persistence-capable if its instances can be stored in a datastore by a JDO implementation. However, just because a class is persistence-capable doesn't mean that all its instances have to be persistent; it just means the option is there. Whether a particular instance is persistent depends on the application. It's similar to Java serialization. Just because a class implements the Serializable interface doesn't mean that all its instances have to be serialized.
However, the intention of JDO is not to expect the developer to worry about making a class persistence-capable; it's a tedious job better left to tooling.
You can create a persistence-capable class in three main ways:
-
Source code generation: With this method, the source code for a class is generated from scratch. This approach works well if the object model is defined in a modeling tool and is being automatically generated, or the datastore schema already exists and the object model can be generated from it. Tools supplied by the JDO implementation would be used to generate source code adhering to the persistence-capable contract. The drawback of this approach is that it won't work for existing classes and won't appeal to those who like to write their own code.
-
Source code preprocessing: With this method, existing source code is preprocessed and updated. This approach works well if the source code for a class is available. Tools supplied by the JDO implementation would be used to read the original source code and update it to adhere to the persistence-capable contract. The drawback of this approach is that it won't work unless the original source code is available, but it does have the benefit that a developer can write his or her own source code. Typically, the preprocessing is a precompilation step in the build process, and the generated code may be kept to aid in debugging.
-
Byte code enhancement: With this method, the compiled Java byte code for a class is enhanced directly. This approach works well even if the source code is not available. Tools supplied by the JDO implementation would be used to read a class file and insert additional byte code directly to make the class adhere to the persistence-capable contract. This approach has the benefit of being completely transparent to the developer, and the enhancement is simply a post-compilation step in the build process. Although the JDO specification requires that an enhanced class still function correctly when debugged against the original source code, some developers may be distrustful if they can't see the actual code for what has been changed (although they could, of course, always decompile the enhanced class file afterward).
Byte code enhancement is the approach used by the JDO reference implementation available from SUN Microsystems, and the enhancement tool is available for any developer to use. Some JDO implementations may provide their own enhancement tools also. Figure 3-2 provides a schematic of how the byte code enhancement process works.
Figure 3-2. The byte code enhancement process.
The Java classes are compiled using a Java compiler to generate class files. The byte code enhancement tool reads the class files along with the JDO metadata for the classes (this metadata is explained in Section 3.3.1) and either updates the existing class files or creates new ones. The "enhanced" class files are then loaded by a JVM along with the JDO implementation and the application. The application can then use JDO to store instances of the persistence-capable classes in the datastore.