Now we have a datastore in which we can store instances of our
classes. Each application needs to acquire a PersistenceManager
to access and update the
datastore. Example 1-8
provides the source for the MediaManiaApp
class, which serves as the base
class for each application in this book. Each application is a concrete
subclass of MediaManiaApp
that
implements its application logic in the execute( )
method.
MediaManiaApp
has a constructor
that loads the properties from jdo.properties (line [1]). After loading properties from the file, it
calls getPropertyOverrides( )
and
merges the returned properties into jdoproperties
. An application subclass can
redefine getPropertyOverrides( )
to
provide any additional properties or change properties that are set in
the jdo.properties file. The
constructor gets a PersistenceManagerFactory
(line [2]) and then acquires a PersistenceManager
(line [3]). We also provide the getPersistenceManager( )
method to access the
PersistenceManager
from outside the
MediaManiaApp
class. The Transaction
associated with the PersistenceManager
is acquired on line
[4].
The application subclasses make a call to executeTransaction( )
, defined in the MediaManiaApp
class. This method begins a
transaction on line [5]. It then calls
execute( )
on line [6], which will execute the subclass-specific
functionality.
We chose this particular design for application classes to simplify and reduce the amount of redundant code in the examples for establishing an environment to run. This is not required in JDO; you can choose an approach that is best suited for your application environment.
After the return from the execute(
)
method (implemented by a subclass), an attempt is made to
commit the transaction (line [7]). If
any exceptions are thrown, the transaction is rolled back and the
exception is printed to the error stream.
Example 1-8. MediaManiaApp base class
package com.mediamania; import java.io.FileInputStream; import java.io.InputStream; import java.util.Properties; import java.util.Map; import java.util.HashMap; import javax.jdo.JDOHelper; import javax.jdo.PersistenceManagerFactory; import javax.jdo.PersistenceManager; import javax.jdo.Transaction; public abstract class MediaManiaApp { protected PersistenceManagerFactory pmf; protected PersistenceManager pm; protected Transaction tx; public abstract void execute( ); // defined in concrete application subclasses protected static Map getPropertyOverrides( ) { return new HashMap( ); } public MediaManiaApp( ) { try { InputStream propertyStream = new FileInputStream("jdo.properties"); Properties jdoproperties = new Properties( ); jdoproperties.load(propertyStream); [1] jdoproperties.putAll(getPropertyOverrides( )); pmf = JDOHelper.getPersistenceManagerFactory(jdoproperties); [2] pm = pmf.getPersistenceManager( ); [3] tx = pm.currentTransaction( ); [4] } catch (Exception e) { e.printStackTrace(System.err); System.exit(-1); } } public PersistenceManager getPersistenceManager( ) { return pm; } public void executeTransaction( ) { try { tx.begin( ); [5] execute( ); [6] tx.commit( ); [7] } catch (Throwable exception) { exception.printStackTrace(System.err); if (tx.isActive()) tx.rollback( ); } } }
Let’s examine a simple application, called CreateMovie
, that makes a single Movie
instance persistent, as shown in Example 1-9. The functionality
of the application is placed in execute(
)
. After constructing an instance of CreateMovie
, we call executeTransaction( )
, which is defined in
the MediaManiaApp
base class. It
makes a call to execute( )
, which
will be the method defined in this class. The execute( )
method instantiates a single
Movie
instance on line [5]. Calling the PersistenceManager
method makePersistent( )
on line [6] makes the Movie
instance persistent. If the
transaction commits successfully in executeTransaction( )
, the Movie
instance will be stored in the
datastore.
Example 1-9. Creating a Movie instance and making it persistent
package com.mediamania.prototype; import java.util.Calendar; import java.util.Date; import com.mediamania.MediaManiaApp; public class CreateMovie extends MediaManiaApp { public static void main(String[] args) { CreateMovie createMovie = new CreateMovie( ); createMovie.executeTransaction( ); } public void execute( ) { Calendar cal = Calendar.getInstance( ); cal.clear( ); cal.set(Calendar.YEAR, 1997); Date date = cal.getTime( ); Movie movie = new Movie("Titanic", date, 194, "PG-13", "historical, drama"); [5] pm.makePersistent(movie); [6] } }
Now let’s examine a larger application. LoadMovies
, shown in Example 1-10, reads a file
containing movie data and creates multiple instances of Movie
. The name of the file is passed to the
application as an argument, and the LoadMovies
constructor initializes a
BufferedReader
to read the data.
The execute( )
method reads one
line at a time from the file and calls parseMovieData( )
, which parses the line of
input data, creates a Movie
instance on line [1], and makes it
persistent on line [2]. When the
transaction commits in executeTransaction(
)
, all of the newly created Movie
instances will be stored in the
datastore.
Example 1-10. LoadMovies
package com.mediamania.prototype; import java.io.FileReader; import java.io.BufferedReader; import java.util.Calendar; import java.util.Date; import java.util.StringTokenizer; import javax.jdo.PersistenceManager; import com.mediamania.MediaManiaApp; public class LoadMovies extends MediaManiaApp { private BufferedReader reader; public static void main(String[] args) { LoadMovies loadMovies = new LoadMovies(args[0]); loadMovies.executeTransaction( ); } public LoadMovies(String filename) { try { FileReader fr = new FileReader(filename); reader = new BufferedReader(fr); } catch (Exception e) { System.err.print("Unable to open input file "); System.err.println(filename); e.printStackTrace( ); System.exit(-1); } } public void execute( ) { try { while ( reader.ready( ) ) { String line = reader.readLine( ); parseMovieData(line); } } catch (java.io.IOException e) { System.err.println("Exception reading input file"); e.printStackTrace(System.err); } } public void parseMovieData(String line) { StringTokenizer tokenizer = new StringTokenizer(line, ";"); String title = tokenizer.nextToken( ); String dateStr = tokenizer.nextToken( ); Date releaseDate = Movie.parseReleaseDate(dateStr); int runningTime = 0; try { runningTime = Integer.parseInt(tokenizer.nextToken( )); } catch (java.lang.NumberFormatException e) { System.err.print("Exception parsing running time for "); System.err.println(title); } String rating = tokenizer.nextToken( ); String genres = tokenizer.nextToken( ); Movie movie = new Movie(title, releaseDate, runningTime, rating, genres); [1] pm.makePersistent(movie); [2] } }
The movie data is in a file with the following format:
movie title;release date;running time;movie rating;genre1,genre2,genre3
The format to use for release dates is maintained in the
Movie
class, so parseReleaseDate( )
is called to create a
Date
instance from the input data.
A movie is described by one or more genres, which are listed at the
end of the line of data.
Now let’s access the Movie
instances in the datastore to verify that they were stored
successfully. There are several ways to access instances in
JDO:
Iterate an extent
Navigate the object model
Execute a query
An extent is a facility used to access all
the instances of a particular class or the class and all its
subclasses. If the application wants to access only a subset of the
instances, a query can be executed with a filter that constrains the
instances returned to those that satisfy a Boolean predicate. Once the
application has accessed an instance from the datastore, it can
navigate to related instances in the datastore by traversing through
references and iterating collections in the object model. Instances
that are not yet in memory are read from the datastore on demand.
These facilities for accessing instances are often used in
combination, and JDO ensures that each persistent instance is
represented in the application memory only once per PersistenceManager
. Each PersistenceManager
manages a single
transaction context.
JDO provides the Extent
interface for accessing the extent of a class. The extent allows
access to all of the instances of a class, but using an extent does
not imply that all the instances are in memory. The PrintMovies
application, provided in Example 1-11, uses the
Movie
extent.
Example 1-11. Iterating the Movie extent
package com.mediamania.prototype; import java.util.Iterator; import java.util.Set; import javax.jdo.PersistenceManager; import javax.jdo.Extent; import com.mediamania.MediaManiaApp; public class PrintMovies extends MediaManiaApp { public static void main(String[] args) { PrintMovies movies = new PrintMovies( ); movies.executeTransaction( ); } public void execute( ) { Extent extent = pm.getExtent(Movie.class, true); [1] Iterator iter = extent.iterator( ); [2] while (iter.hasNext( )) { Movie movie = (Movie) iter.next( ); [3] System.out.print(movie.getTitle( )); System.out.print(";"); System.out.print(movie.getRating( )); System.out.print(";"); System.out.print(movie.formatReleaseDate( ) ); System.out.print(";"); System.out.print(movie.getRunningTime( )); System.out.print(";"); System.out.println(movie.getGenres( )); [4] Set cast = movie.getCast( ); [5] Iterator castIterator = cast.iterator( ); while (castIterator.hasNext( )) { Role role = (Role) castIterator.next( ); [6] System.out.print("\t"); System.out.print(role.getName( )); System.out.print(", "); System.out.println(role.getActor().getName( )); [7] } } extent.close(iter); [8] } }
On line [1] we acquire an
Extent
for the Movie
class from the PersistenceManager
. The second parameter
indicates whether to include instances of Movie
subclasses. A value of false
causes only Movie
instances to be returned, even if
there are instances of subclasses. Though we don’t currently have
any classes that extend the Movie
class, providing a value of true
will return instances of any such classes that we may define in the
future. The Extent
interface has
the iterator( )
method, which we call on line [2]
to acquire an Iterator
that will
access each element of the extent. Line [3] uses the Iterator
to access Movie
instances. The application can then
perform operations on the Movie
instance to acquire data about the movie to print. For example, on
line [4] we call getGenres( )
to get the genres associated
with the movie. On line [5] we
acquire the set of Role
s. We
acquire a reference to a Role
on
line [6] and then print the role’s
name. On line [7] we navigate to
the Actor
for that role by
calling getActor( )
, which we
defined in the Role
class. We
then print the actor’s name.
Once the application has completed iteration through the
extent, line [8] closes the
Iterator
to relinquish any
resources required to perform the extent iteration. Multiple
Iterator
instances can be used
concurrently on an Extent
. This
method closes a specific Iterator
; closeAll( )
closes
all the Iterator
instances
associated with an Extent
.
Example 1-11
demonstrates iteration of the Movie
extent. But on line [6] we also navigate to a set of related
Role
instances by iterating a
collection in our object model. On line [7] we use the Role
instance to navigate through a
reference to the related Actor
instance. Line [5] and [7] demonstrate, respectively, traversal of
to-many and to-one
relationships. A relationship from one class to another has a
cardinality that indicates whether there are one or multiple
associated instances. A reference is used for a
cardinality of one, and a collection is used
when there can be more than one instance.
The syntax needed to access these related instances corresponds to the standard practice of navigating instances in memory. The application does not need to make any direct calls to JDO interfaces between lines [3] and [7]. It simply traverses among objects in memory. The related instances are not read from the datastore and instantiated in memory until they are accessed directly by the application. Access to the datastore is transparent; instances are brought into memory on demand. Some implementations provide facilities separate from the Java interface that allow you to influence the implementation’s access and caching algorithms. Your Java application is insulated from these optimizations, but it can take advantage of them to affect its overall performance.
The access of related persistent instances in a JDO environment is identical to the access of transient instances in a non-JDO environment, so you can write your software in a manner that is independent of its use in a JDO environment. Existing software written without any knowledge of JDO or any other persistence concerns is able to navigate objects in the datastore through JDO. This capability yields dramatic increases in development productivity and allows existing software to be incorporated into a JDO environment quickly and easily.
It is also possible to perform a query on an Extent
. The JDO Query
interface is used to select a subset
of the instances that meet certain criteria. The remaining examples
in this chapter need to access a specific Actor
or Movie
based on a unique name. These
methods, shown in Example
1-12, are virtually identical; getActor( )
performs a query to get an
Actor
based on a name, and
getMovie( )
performs a query to
get a Movie
based on a
name.
Example 1-12. Query methods in the PrototypeQueries class
package com.mediamania.prototype; import java.util.Collection; import java.util.Iterator; import javax.jdo.PersistenceManager; import javax.jdo.Extent; import javax.jdo.Query; public class PrototypeQueries { public static Actor getActor(PersistenceManager pm, String actorName) { Extent actorExtent = pm.getExtent(Actor.class, true); [1] Query query = pm.newQuery(actorExtent, "name == actorName"); [2] query.declareParameters("String actorName"); [3] Collection result = (Collection) query.execute(actorName); [4] Iterator iter = result.iterator( ); Actor actor = null; if (iter.hasNext()) actor = (Actor)iter.next( ); [5] query.close(result); [6] return actor; } public static Movie getMovie(PersistenceManager pm, String movieTitle) { Extent movieExtent = pm.getExtent(Movie.class, true); Query query = pm.newQuery(movieExtent, "title == movieTitle"); query.declareParameters("String movieTitle"); Collection result = (Collection) query.execute(movieTitle); Iterator iter = result.iterator( ); Movie movie = null; if (iter.hasNext()) movie = (Movie)iter.next( ); query.close(result); return movie; } }
Let’s examine getActor( )
.
On line [1] we get a reference to
the Actor
extent. Line [2] creates an instance of Query
using the newQuery( )
method defined in the PersistenceManager
interface. The query is
initialized with the extent and a query filter to apply to the
extent.
The name
identifier in the
filter is the name
field in the
Actor
class. The namespace used
to determine how to interpret the identifier is based on the class
of the Extent
used to initialize
the Query
instance. The filter
expression requires that an Actor
’s name
field is equal to actorName
. In the filter we can use the
==
operator directly to compare
two String
s, instead of using the
Java syntax (name.equals(actorName)
).
The actorName
identifier is
a query parameter, which is declared on line
[3]. A query parameter lets you
provide a value to be used when the query is executed. We have
chosen to use the same name, actorName
, for the method parameter and
query parameter. This practice is not required, and there is no
direct association between the names of our Java method parameters
and our query parameters. The query is executed on line [4], passing getActor( )
’s actorName
parameter as the value to use
for the actorName
query
parameter.
The result type of Query.execute(
)
is declared as Object
. In JDO 1.0.1,
the returned instance is always a Collection
, so we cast the query result to
a Collection
. It is declared in
JDO 1.0.1 to return Object
, to
allow for a future extension of returning a value other than a
Collection
. Our method then
acquires an Iterator
and, on line
[5], attempts to access an element.
We assume here that there can only be a single Actor
instance with a given name. Before
returning the result, line [6]
closes the query result to relinquish any associated resources. If
the method finds an Actor
instance with the given name, the instance is returned. Otherwise,
if the query result has no elements, a null
is returned.
Now let’s examine two applications that modify instances in the datastore. Once an application has accessed an instance from the datastore in a transaction, it can modify one or more fields of the instance. When the transaction commits, all modifications that have been made to instances are propagated to the datastore automatically.
The UpdateWebSite
application
provided in Example 1-13
is used to set the web site associated with a movie. It takes two
arguments: the first is the movie’s title, and the second is the
movie’s web site URL. After initializing the application instance,
executeTransaction( )
is called,
which calls the execute( )
method
defined in this class.
Line [1] calls getMovie( )
(defined in Example 1-12) to retrieve the
Movie
with the given title. If
getMovie( )
returns null
, the application reports that it could
not find a Movie
with the given
title and returns. Otherwise, on line [2] we call setWebSite( )
(defined for the Movie
class in Example 1-1), which sets the
webSite
field of Movie
to the parameter value. When executeTransaction( )
commits the
transaction, the modification to the Movie
instance is propagated to the
datastore automatically.
Example 1-13. Modifying an attribute
package com.mediamania.prototype; import com.mediamania.MediaManiaApp; public class UpdateWebSite extends MediaManiaApp { private String movieTitle; private String newWebSite; public static void main (String[] args) { String title = args[0]; String website = args[1]; UpdateWebSite update = new UpdateWebSite(title, website); update.executeTransaction( ); } public UpdateWebSite(String title, String site) { movieTitle = title; newWebSite = site; } public void execute( ) { Movie movie = PrototypeQueries.getMovie(pm, movieTitle); [1] if (movie == null) { System.err.print("Could not access movie with title of "); System.err.println(movieTitle); return; } movie.setWebSite(newWebSite); [2] } }
As you can see in Example
1-13, the application does not need to make any direct JDO
interface calls to modify the Movie
field. This application accesses an instance and calls a method to
modify the web site field. The method modifies the field using
standard Java syntax. No additional programming is necessary prior to
commit in order to propagate the data to the datastore. The JDO
environment propagates the modifications automatically. This
application performs an operation on persistent instances, yet it does
not directly import or use any JDO interfaces.
Now let’s examine a larger application, called LoadRoles
, that exhibits several JDO
capabilities. LoadRoles
, shown in
Example 1-14, is
responsible for loading information about the movie roles and the
actors who play them. LoadRoles
is
passed a single argument that specifies the name of a file to read,
and the constructor initializes a BufferedReader
to read the file. It reads
the text file, which contains one role per line, in the following
format:
movie title;actor's name;role name
Usually, all the roles associated with a particular movie are
grouped together in this file; LoadRoles
performs a small optimization to
determine whether the role information being processed is for the same
movie as the previous role entry in the file.
Example 1-14. Instance modification and persistence-by-reachability
package com.mediamania.prototype; import java.io.FileReader; import java.io.BufferedReader; import java.util.StringTokenizer; import com.mediamania.MediaManiaApp; public class LoadRoles extends MediaManiaApp { private BufferedReader reader; public static void main(String[] args) { LoadRoles loadRoles = new LoadRoles(args[0]); loadRoles.executeTransaction( ); } public LoadRoles(String filename) { try { FileReader fr = new FileReader(filename); reader = new BufferedReader(fr); } catch(java.io.IOException e){ System.err.print("Unable to open input file "); System.err.println(filename); System.exit(-1); } } public void execute( ) { String lastTitle = ""; Movie movie = null; try { while (reader.ready( )) { String line = reader.readLine( ); StringTokenizer tokenizer = new StringTokenizer(line, ";"); String title = tokenizer.nextToken( ); String actorName = tokenizer.nextToken( ); String roleName = tokenizer.nextToken( ); if (!title.equals(lastTitle)) { movie = PrototypeQueries.getMovie(pm, title); [1] if (movie == null) { System.err.print("Movie title not found: "); System.err.println(title); continue; } lastTitle = title; } Actor actor = PrototypeQueries.getActor(pm, actorName); [2] if (actor == null) { actor = new Actor(actorName); [3] pm.makePersistent(actor); [4] } Role role = new Role(roleName, actor, movie); [5] } } catch (java.io.IOException e) { System.err.println("Exception reading input file"); System.err.println(e); return; } } }
The execute( )
method reads
each entry in the file. First, it checks to see whether the new
entry’s movie title is the same as the previous entry. If it is not,
line [1] calls getMovie( )
to access the Movie
with the new title. If a Movie
with that title does not exist in the
datastore, the application prints an error message and skips over the
entry. On line [2] we attempt to
access an Actor
instance with the
specified name. If no Actor
in the
datastore has this name, a new Actor
is created and given this name on line
[3], and made persistent on line
[4].
Up to this point in the application, we have just been reading
the input file and looking up instances in the datastore that have
been referenced by a name in the file. We perform the real task of the
application on line [5], where we
create a new Role
instance. The
Role
constructor was defined in
Example 1-3; it is
repeated here so that we can examine it in more detail:
public Role(String name, Actor actor, Movie movie) { this.name = name; [1] this.actor = actor; [2] this.movie = movie; [3] actor.addRole(this); [4] movie.addRole(this); [5] }
Line [1] initializes the
name
of the Role
. Line [2] establishes a reference to the associated
Actor
, and line [3] establishes a reference to the associated
Movie
instance. The relationships
between Actor
and Role
and between Movie
and Role
are bidirectional, so it is also
necessary to update the other side of each relationship. On line
[4] we call addRole( )
on actor
, which adds this Role
to the roles
collection in the Actor
class. Similarly, line [5] calls addRole(
)
on movie
to add this
Role
to the cast
collection field in the Movie
class. Adding the Role
as an element in Actor.roles
and Movie.cast
causes a modification to the
instances referenced by actor
and
movie
.
The Role
constructor
demonstrates that you can establish a relationship to an instance
simply by initializing a reference to it, and you can establish a
relationship with more than one instance by adding references to a
collection. This process is how relationships are represented in Java
and is supported directly by JDO. When the transaction commits, the
relationships established in memory are preserved in the
datastore.
Upon return from the Role
constructor, execute( )
processes
the next entry in the file. The while
loop terminates once we have exhausted
the contents of the file.
You may have noticed that we never called makePersistent( )
on
the Role
instances we created.
Still, at commit, the Role
instances are stored in the datastore because JDO supports
persistence-by-reachability . Persistence-by-reachability causes any transient
(nonpersistent) instance of a persistent class to become persistent at
commit if it is reachable (directly or indirectly) by a persistent
instance. Instances are reachable through either a reference or
collection of references. The set of all instances reachable from a
given instance is an object graph that is called the instance’s
complete closure of related instances. The
reachability algorithm is applied to all persistent instances
transitively through all their references to instances in memory,
causing the complete closure to become persistent.
Removing all references to a persistent instance does not automatically delete the instance. You need to delete instances explicitly, which we cover in the next section. If you establish a reference from a persistent instance to a transient instance during a transaction, but you change this reference and no persistent instances reference the transient instance at commit, it remains transient.
Persistence-by-reachability lets you write a lot of your software without having any explicit calls to JDO interfaces to store instances. Much of your software can focus on establishing relationships among the instances in memory, and the JDO implementation takes care of storing any new instances and relationships you establish among the instances in memory. Your applications can construct fairly complex object graphs in memory and make them persistent simply by establishing a reference to the graph from a persistent instance.
Now let’s examine an application that deletes some instances
from the datastore. In Example
1-15, the DeleteMovie
application is used to delete a Movie
instance. The title of the movie to
delete is provided as the argument to the program. Line [1] attempts to access the Movie
instance. If no movie with the title
exists, the application reports an error and returns. On line
[6] we call deletePersistent( )
to delete the Movie
instance
itself.
Example 1-15. Deleting a Movie from the datastore
package com.mediamania.prototype; import java.util.Collection; import java.util.Set; import java.util.Iterator; import javax.jdo.PersistenceManager; import com.mediamania.MediaManiaApp; public class DeleteMovie extends MediaManiaApp { private String movieTitle; public static void main(String[] args) { String title = args[0]; DeleteMovie deleteMovie = new DeleteMovie(title); deleteMovie.executeTransaction( ); } public DeleteMovie(String title) { movieTitle = title; } public void execute( ) { Movie movie = PrototypeQueries.getMovie(pm, movieTitle); [1] if (movie == null) { System.err.print("Could not access movie with title of "); System.err.println(movieTitle); return; } Set cast = movie.getCast( ); [2] Iterator iter = cast.iterator( ); while (iter.hasNext( )) { Role role = (Role) iter.next( ); Actor actor = role.getActor( ); [3] actor.removeRole(role); [4] } pm.deletePersistentAll(cast); [5] pm.deletePersistent(movie); [6] } }
But it is also necessary to delete the Role
instances associated with the Movie
. In addition, since an Actor
includes a reference to the Role
instance, it is necessary to remove
this reference. On line [2] we access
the set of Role
instances
associated with the Movie
. We then
iterate through each Role
and
access the associated Actor
on line
[3]. Since we will be deleting the
Role
instance, on line [4] we remove the actor
’s reference to the Role
. On line [5] we make a call to deletePersistentAll(
)
to delete all the Role
instances in the movie’s cast. When we commit the transaction, the
Movie
instance and associated
Role
instances are deleted from the
datastore, and the Actor
instances
associated with the Movie
are
updated so that they no longer reference the deleted Role
instances.
You must call these deletePersistent( )
methods explicitly to delete instances from the datastore. They are
not the inverse of makePersistent(
)
, which uses the persistence-by-reachability algorithm.
Furthermore, there is no JDO datastore equivalent to Java’s garbage
collection, which deletes instances automatically once they are no
longer referenced by any instances in the datastore. Implementing the
equivalent of a persistent garbage collector is a very complex
undertaking, and such systems often have poor performance.
Get Java Data Objects now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.