8.1 Persistence of Instances

A class is persistent if it has been specified in a JDO metadata file and enhanced. An instance of a persistent class can be either transient or persistent. The JDO specification refers to a persistent class as persistence-capable to emphasize that while a class provides support for persistence, it allows instances to be transient or persistent. We just use the phrase persistent class and note that instances can be either transient or persistent. We refer to classes that are not persistent as transient classes. All instances of a transient class are transient.

All instances of transient and persistent classes that you construct in your applications are initially transient. They become persistent explicitly when you pass them to makePersistent( ), or implicitly if they are referenced by a persistent instance at transaction commit.

8.1.1 Explicit Persistence

You can call the following PersistenceManager method to make a transient instance persistent explicitly:

void makePersistent(Object obj);

You must call it in the context of an active transaction, or a JDOUserException is thrown.

Null Parameters

The PersistenceManager interface has methods that are passed references to one or more instances; the parameters are defined as one of the following types: Object, Object[], and Collection. You can pass a null value for these parameters. If you pass a null to a method taking an Object parameter, the method has no effect. If you pass null as the value for a parameter of the Object[] or Collection type, the method throws a NullPointerException. If you pass a non-null Object[] or Collection that contains elements that are null, the operation is applied to the non-null elements and the null elements are ignored.

The following program creates some Studio instances and makes them persistent with makePersistent( ):

package com.mediamania.content;

import com.mediamania.MediaManiaApp;
import javax.jdo.PersistenceManager;

public class LoadStudios extends MediaManiaApp {
    public static void main(String[] args) {
        LoadStudios studios = new LoadStudios(  );
        studios.executeTransaction(  );        
    }
    public void execute(  ) {
        Studio studio = new Studio("Buena Vista");
        pm.makePersistent(studio);
        studio = new Studio("20th Century Fox");
        pm.makePersistent(studio);
        studio = new Studio("DreamWorks SKG");
        pm.makePersistent(studio);
    }
}

You can also call one of the following PersistenceManager methods to make an array or collection of instances persistent:

void makePersistentAll(Object[] objs);
void makePersistentAll(Collection objs);

These methods have no effect on any of the parameter instances that are already persistent and managed by this PersistenceManager. A JDOUserException is thrown if a parameter instance is managed by a different PersistenceManager.

When One or More Instances Fail an Operation

The PersistenceManager interface has several methods that perform operations on an array or collection of objects. These methods include:

  • deletePersistentAll( )

  • evictAll( )

  • makeNontransactionalAll( )

  • makePersistentAll( )

  • makeTransactionalAll( )

  • makeTransientAll( )

  • refreshAll( )

  • retrieveAll( )

Some of these methods can be called without any parameter instances, implying the operation is applied to all instances managed by the PersistenceManager.

The operation is attempted on all of the instances, even if the operation fails for one or more of them. The succeeding instances transition to a specific lifecycle state based on their current state and the operation being applied. Chapter 11 covers lifecycle states and transitions. Instances that fail the operation remain in their current state, and the method throws a JDOUserException with a nested exception array that contains a nested exception for each failing instance.

The following program makes an array of RentalCode instances persistent:

package com.mediamania.store;

import com.mediamania.MediaManiaApp;
import javax.jdo.PersistenceManager;
import java.math.BigDecimal;

public class LoadRentalCodes extends MediaManiaApp {
    private static BigDecimal cost6 = new BigDecimal("6.00");
    private static BigDecimal cost5 = new BigDecimal("5.00");
    private static BigDecimal cost4 = new BigDecimal("4.00");
    private static BigDecimal cost2 = new BigDecimal("2.00");
    private static BigDecimal cost1 = new BigDecimal("1.00");

    private static RentalCode[] codes = {
        new RentalCode("Hot",      1, cost6, cost6),
        new RentalCode("New",      2, cost5, cost4),
        new RentalCode("Recent",   4, cost5, cost2),
        new RentalCode("Standard", 5, cost4, cost2),
        new RentalCode("Oldie",    7, cost2, cost1) 
    };
    public static void main(String[] args) {
        LoadRentalCodes loadRentalCodes = new LoadRentalCodes(  );
        loadRentalCodes.executeTransaction(  );
    }
    public void execute(  ) {
        pm.makePersistentAll(codes);
    }
}

It is a common mistake to pass an array or collection to makePersistent( ), which has a single instance parameter and makes it persistent. In this case, makePersistent( ) throws an exception because, although arrays and collections are objects, they cannot be persistent by themselves. So, be sure that you call makePersistentAll( ) when making an array or collection of instances persistent. Each PersistenceManager operation that can accept multiple instances, passed by an array or collection, has a method name that ends with the word All.

8.1.2 Persistence-by-Reachability

Within application memory, instances of transient classes and the transient and persistent instances of persistent classes can reference one another. When a persistent instance is committed to the datastore, transient instances of persistent classes that are referenced by persistent fields of the flushed instance also become persistent. This behavior propagates to all instances in the closure of instances reachable through persistent fields. This behavior is called persistence-by-reachability.

Figure 8-1 illustrates persistence-by-reachability in an instance diagram.

Figure 8-1. Persistence-by-reachability
figs/jdo_0801.gif

Each rectangle represents an instance, identified by the names i1 through i9. The UML stereotype notation of «stereotype» is used to indicate whether the class and instance are transient or persistent. The specific class of each instance is not identified, but the topmost stereotype indicates whether the class is persistent or transient. Only i4 is an instance of a transient class; all the others are instances of a persistent class. The stereotype below the instance identifier indicates whether the specific instance is transient or persistent. In the top half of Figure 8-1, i1 is persistent and all other instances are transient. The field c1 is a collection that contains references to i5, i6, and i7. Instance i2 contains a transient field named f2, and it references i3.

The top half of the diagram indicates the persistence of instances in memory prior to commit; the bottom half specifies their persistence after commit. The instances identified as transient in the bottom half of the figure are not in the datastore. Each reference depicted in this model is a persistent field, except for the f2 field in instance i2. The reachability algorithm does not include transient instances referenced by a transient fields. As you can see, the reachability algorithm transitively traverses through references and collections, making all instances of persistent classes persistent. Instance i4 is an instance of a transient class, so it does not become persistent. Instance i3, referenced by the transient field f2, also does not become persistent.

When you explicitly make an instance persistent, any transient instances that are reachable transitively via persistent fields of this instance become provisionally persistent. The reachability algorithm runs again at commit. Any instance that was made provisionally persistent during the transaction, but is no longer reachable from a persistent instance at commit, reverts to a transient instance.

The following program loads information about new movies into the database, making extensive use of persistence-by-reachability. In addition, it creates a RentalItem instance for each item that will be rented to customers. A large percentage of the code deals strictly with parsing the input data. Line [1] creates a Movie instance, which is then made persistent on line [2]. After reading a line of data with movie-content data, the program reads some information about the particular formats of the movie (e.g., DVD and VHS), represented by a MediaItem instance. The parseMediaItemData( ) method reads the information required to initialize a MediaItem instance. Line [4] creates the MediaItem instance. The input data then contains a line for each rental unit that provides its unique serial number. Line [5] creates RentalItem instances with the provided serial number and line [6] associates it with the MediaItem instance. When parseMediaItemData( ) returns the MediaItem instance, line [3] associates it with the Movie instance.

package com.mediamania.store;

import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;
import java.math.BigDecimal;
import javax.jdo.PersistenceManager;
import com.mediamania.MediaManiaApp;
import com.mediamania.content.*;

public class LoadNewMovies extends MediaManiaApp {
    private BufferedReader  reader;

    public static void main(String[] args) {
        LoadNewMovies loadMovies = new LoadNewMovies(args[0]);
        loadMovies.executeTransaction(  );
    }
    public LoadNewMovies(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);
            System.exit(-1);
        }
    }
    public void execute(  ) {
        try {
            while (reader.ready(  )) {
                String line = reader.readLine(  );
                parseMovieData(line);
            }
        } catch (IOException e) {
            System.err.println("Exception reading input file");
            System.err.println(e);
        }
        // when execute returns and the transaction commits, each of the
        // transient Studio, MediaPerson, MediaItem, RentalItem instances
        // associated with the Movie instance we explicitly made persistent
        // will become persistent through reachability
    }

    public void parseMovieData(String line) throws IOException {
        StringTokenizer tokenizer = new StringTokenizer(line, ";");
        String title = tokenizer.nextToken(  );
        String studioName = tokenizer.nextToken(  );
        Studio studio = ContentQueries.getStudioByName(pm, studioName);
        if (studio == null)
            studio = new Studio(studioName);  // creates a transient Studio
        String dateStr = tokenizer.nextToken(  );
        Date releaseDate = Movie.parseReleaseDate(dateStr);
        String rating = tokenizer.nextToken(  );
        String reasons = tokenizer.nextToken(  );
        String genres = tokenizer.nextToken(  );
        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 directorName = tokenizer.nextToken(  );
        MediaPerson director = ContentQueries.getMediaPerson(pm, directorName);
        if (director == null) {
            System.err.print("Director named ");
            System.err.print(directorName);
            System.err.print(" for movie ");
            System.err.print(title);
            System.err.println(" not found in the database");
            director = new MediaPerson(directorName); //creates transient MediaPerson
        }
        Movie movie = new Movie(title, studio, releaseDate, rating, reasons,     [1]
                           genres, runningTime, director); // creates transient Movie
        pm.makePersistent(movie);     [2]
        
        int numFormats = 0;
        try {
            numFormats = Integer.parseInt(tokenizer.nextToken(  ));
        } catch (java.lang.NumberFormatException e) {
            System.err.print("Exception parsing number of formats for ");
            System.err.println(title);
        }
        for (int i = 0; i < numFormats; ++i) {
            MediaItem mediaItem = parseMediaItemData(movie);
            movie.addMediaItem(mediaItem); // adds transient MediaItem     [3]
        }
    }
// the following method returns a transient MediaItem
// and a set of associated transient RentalItems
    private MediaItem parseMediaItemData(MediaContent content)
      throws IOException {
        String line = reader.readLine(  );
        StringTokenizer tokenizer = new StringTokenizer(line, ";");
        String format = tokenizer.nextToken(  );
        String priceString = tokenizer.nextToken(  );
        BigDecimal price = new BigDecimal(priceString);
        String rentalCodeName = tokenizer.nextToken(  );
        RentalCode rentalCode = StoreQueries.getRentalCode(pm, rentalCodeName);
        int Nrentals = 0;
        try {
            Nrentals = Integer.parseInt(tokenizer.nextToken(  ));
        } catch (java.lang.NumberFormatException e) {
            System.err.print("Exception parsing # of rentals for ");
            System.err.println(content.getTitle(  ));
        }
        int NforSale = 0;
        try {
            NforSale = Integer.parseInt(tokenizer.nextToken(  ));
        } catch (java.lang.NumberFormatException e) {
            System.err.print("Exception parsing # for sale of ");
            System.err.println(content.getTitle(  ));
        }
        MediaItem mediaItem = new MediaItem(content, format, price,     [4]
                                            rentalCode, NforSale);
        for (int r = 0; r < Nrentals; ++r) {
            String serialNumber = reader.readLine(  );
            RentalItem rentalItem = new RentalItem(mediaItem, serialNumber);     [5]
            mediaItem.addRentalItem(rentalItem); // add transient RentalItem     [6]
        }
        return mediaItem;
    }
}

When the Movie instance is made persistent on line [2], a MediaPerson and Studio instance are created and referenced by the Movie instance if they are not found in the database. In this case, when the call is made to makePersistent( ) on line [2], the MediaPerson and Studio instances become provisionally persistent. References are established from the newly persistent Movie instance to MediaItem instances. References are then established from these MediaItem instances to RentalItem instances on line [6]. The reachability algorithm runs when the transaction commits. If a MediaPerson or Studio instance is still associated with the Movie instance at commit, it becomes persistent. Further, each MediaItem instance associated with the Movie instance and each RentalItem instance associated with each such MediaItem instance are reachable from the Movie instance and become persistent.

A major benefit of persistence-by-reachability is that most of your application can be written entirely independent of JDO, without making any explicit calls to JDO interfaces. Most of your application can use standard Java practices to create and associate instances in memory, without knowing that a datastore or transaction is involved. The JDO implementation automatically handles all the work of storing new persistent instances and associations that you have established established between persistent instances.