The InstanceCallbacks Interface


The JDO runtime interacts with the application data objects primarily through the javax.jdo.spi.PersistenceCapable interface, which enhancement adds to the application data class. The JDO runtime may also interact with application data objects through the InstanceCallbacks interface, an optional interface that the developer may add to the application data class.

If an application data class implements the InstanceCallbacks interface, then JDO calls the methods of the interface when specific events in the application data object's life cycle occur. Several types of actions can trigger the callbacks. The application may call methods in the Transaction or PersistenceManager interfaces that cause the callbacks. For example, calls to commit or rollback on the Transaction interface, or calls to evict or makeTransactional on the PersistenceManager interface, may cause one or more of the methods in InstanceCallbacks to be called. Actions arising from transparent persistence may also cause the callbacks. For example, iterating a collection or using the value of a persistent field may cause a callback.

This section describes the specific context of each callback method. It also describes some design problems that can be solved, and some that cannot be solved, by using one or more callback methods.

As the UML class diagram in Figure 7-2 illustrates, there are only four methods in the InstanceCallbacks interface. As the names of the callback methods indicate, the events that trigger a callback are related to actions on the persistent state of individual application data objects.


Figure 7-2: The class diagram of the InstanceCallbacks interface

Caution

An easily overlooked bug when implementing the InstanceCallbacks interface occurs if the application data class fails to declare that it implements the interface. In this case, the class compiles and enhances just fine, but the JDO runtime never calls the callback methods.

The jdoPostLoad Callback

After JDO loads the fields in the default fetch group, it calls the application data object's jdoPostLoad method.

 public void jdoPostLoad() 

The code within the jdoPostLoad method is not enhanced for transparent persistence. As a result, the method should access only the persistent fields in the default fetch group and the application-defined key fields, if any. It should not access the persistent fields that are outside the default fetch group, and it should not access other application data objects.

Note

As described in Chapter 5, the JDO metadata defines explicitly, or by default, which member fields in the application data class are part of the object's default fetch group.

In carrying out transparent persistence, the implementation makes the choice whether and when to load the default fetch group. Because the purpose of the default fetch group is to optimize access to the database, implementations usually read all the fields in the default fetch group together, but this behavior is not required. It is optional for the JDO implementation to load the default fetch group. The timing of the load of the default fetch group may vary widely across objects as the JDO runtime executes algorithms designed to optimize access to the database. As a result, the application cannot know when the callback will occur or even whether it will occur. Nonetheless, most implementations transparently load the default fetch group of a persistent object under the following circumstances:

  • In response to a call to refresh in an optimistic transaction.

  • Before returning to the application the value of a persistent field in the default fetch group, if the default fetch group has not been loaded.

  • Before allowing the application to change the value of a persistent field in the default fetch group, if the default fetch group has not been loaded.

  • In response to the object becoming transactional in a datastore transaction. In a datastore transaction, any existing persistent state of the persistent object is discarded when the object becomes transactional, and for that reason, a reload of the default fetch group usually follows either immediately or when a persistent field in the default fetch group is accessed or modified.

  • Before allowing the object to be serialized, if the default fetch group has not been loaded.

  • In response to a call to retrieve, if the default fetch group has not been loaded.

As a result of the frequent opportunities to load the default fetch group and the inherent usefulness of doing so, applications can rely on the callback to jdoPostLoad to occur regularly in most implementations.

The jdoPreStore Callback

The JDO runtime calls the application data object's jdoPreStore method after the transaction is asked to commit and before the cached persistent state is transferred to the datastore.

 public void jdoPreStore() 

The code within the jdoPreStore method is enhanced for transparent persistence. If the method modifies persistent fields within its object, the updated values are sent to the datastore during the commit operation that is in process. In addition, the runtime context of this callback permits the code to access persistence managers and other application data objects. The application data object must be either persistent-new or persistent-dirty to receive this callback. In either case, the JDO identity is available.

The jdoPreClear Callback

The JDO runtime calls the application data object's jdoPreClear method before it resets the object's persistent fields to their Java default values.

 public void jdoPreClear() 

The code within the jdoPreClear method is not enhanced for transparent persistence. For that reason, the implementation code should be careful in making assumptions about the state of persistent fields. A persistent field may not have been loaded prior to this call and may therefore hold its Java default value rather than its managed persistent value.

The generic term for clearing the object's persistent state is eviction. Many actions and events can cause eviction. Eviction may occur transparently upon transaction commit when the transaction's RetainValues property is false, or upon rollback in some cases when the transaction's RestoreValues property is false. The PersistenceManager interface also has the evict and evictAll methods to force eviction. Finally, the implementation is at liberty to evict persistent-nontransactional application data objects at any point that it sees fit as part of its general cache management strategy. Chapter 4 describes the Transaction interface, and Chapter 3 covers the PersistenceManager interface.

When the transaction commits, if the transaction's RetainValues property is false, then objects in any of the following management states are evicted and the callback to jdoPreClear occurs:

  • Persistent-clean

  • Persistent-new

  • Persistent-dirty

  • Persistent-deleted

  • Persistent-new-deleted

If the RetainValues property is true, then the objects in the first three states are not evicted and the callback does not occur. For the last two states, the specification is unclear on the behavior to expect when RetainValues is true. Eviction may or may not take place. If eviction occurs, the callback occurs.

When the transaction rolls back, if the transaction's RestoreValues property is false, then objects in any of the following three states are evicted and the callback to jdoPreClear occurs:

  • Persistent-clean

  • Persistent-dirty

  • Persistent-deleted

Otherwise, if the object is in any of the other management states, or if RestoreValues is true, then eviction does not occur.

The jdoPreDelete Callback

JDO calls the jdoPreDelete method when the application calls the persistence manager's deletePersistent method.

 public void jdoPreDelete() 

The code within the jdoPreDelete callback is enhanced for transparent persistence. The callback occurs before the application data object transitions to one of the deleted states. After an object has been deleted, no persistent fields may be modified, and only application-defined key fields, when present, may be accessed. Within jdoPreDelete, all persistent fields may be accessed and modified.

Ways to Use the InstanceCallbacks Methods

The InstanceCallbacks methods allow the application to react to events in JDO's management of the object. In some cases, responding to these events is the perfect place to take actions desired by the application, but in other cases, the callback methods are not adequate for the application's purpose.

The following sections examine some uses, or attempted uses, for the callback methods and evaluate whether the callback methods are the best place to accomplish the design goal. Applications will certainly use the InstanceCallbacks interface in more ways than the few discussed here, but these appear to be common, or at least commonly attempted.

Validation

Typically, when data is accepted from some noncontrolled source, such as a user or another system, the application usually validates the data before storing it in the datastore. To validate, the application applies rules derived from the business domain.

The earlier that validation occurs, the more likely it is that the source can resubmit correct information. Like applications that do not use JDO, those that use JDO want to validate the information received as early as possible. Depending on the application's architecture, this may occur as the information is received from the source, it may occur as the application accepts the information, or it may occur in the application data objects as or after the information is stored in them. All of these places are stops on the road from the source of the information to the datastore.

Although jdoPreStore is another stop on the same road, because it is the last stop, in most applications it is not the best place for data validation. Nonetheless, it may be better to validate the information in jdoPreStore than never validate it at all. If the validation performed in jdoPreStore succeeds, the callback can simply return.

One of the difficulties of doing validation in jdoPreStore is knowing what to do when validation fails. On the one hand, when JDO is managing the transaction, JDO does not specify a way to stop the transaction's commit within jdoPreStore. On the other hand, when an EJB container is managing the transactions, the container provides a way to reject the transaction's commit. Within a CMT bean, calling setRollbackOnly on the EJBContext object ensures that the transaction cannot commit. To allow the jdoPreStore method to find the EJBContext object, the application might store it in the persistence manager's UserObject property.

Storing Calculation Results in Unmanaged Fields

In some cases, the application stores in unmanaged fields the calculation results that directly or indirectly derive from the object's persistent state. Whenever possible, avoid this design for the simple reason that the code to implement caching is nearly always more complex than it at first appears to be.

The simplest approach to managing transient state that derives from the persistent state is to use lazy evaluation. Listing 7-1 shows a simple example of lazy evaluation for a hypothetical Customer class. The work of constructing the full name is done only once, upon the first request for the full name; however, additional code clears the transient state at the appropriate times. When mutators change the components that the fullName depends on, the mutators clear the transient state. When JDO clears the persistent state, jdoPreClear clears the transient state. As with all caching, the gain is realized only when the method that accesses cached data is called more than once. In this simple case, the gain is roughly zero, since the use of cached state avoids only the string concatenations. In practice, a more time-consuming or resource-consuming calculation is required before it would pay to cache the result in an unmanaged field.

Listing 7-1: Transient State Dependent on Persistent State

start example
 public class Customer implements InstanceCallbacks    {    private String lastName; // persistent field    private String firstName; // persistent field    private String fullName; // unmanaged field    public String getFullName()       {       if (fullName == null)          fullName = firstName + " " + lastName;       return fullName;       }    public void setFirstName(String name)       {       fullname = null;       firstName = name;       }    public void setLastName(String name)       {       fullName = null;       lastName = name;       }    public void jdoPreClear()       {       fullName = null;       }    // the other methods of InstanceCallbacks are defined empty    } 
end example

Performing Background Calculations

There are times when lazy evaluation is not the answer for calculating results. For example, a hypothetical Stock object might have buyAt and sellAt price points that are calculated and stored in unmanaged fields. The calculated prices are not persistent because they vary over time, and there is no reason to remember them beyond a short period of time. They are stored in the object because the computation is too costly to perform unnecessarily. The computation is also too costly to perform upon demand. Instead, the strategy is to precalculate these values in the background while the user is busy with something else and before he needs the values.

It is tempting to see the jdoPostLoad callback as the trigger for the calculation. There is one conclusive reason to avoid this. The jdoPostLoad method is called in response to the user's actions. Consequently, the user's request that indirectly caused the default fetch group to load and the jdoPostLoad method to be called blocks until the jdoPostLoad method has completed. If the object should do the calculation in the background, then jdoPostLoad cannot do the job. Instead, the application should create a background thread to perform the task.

When multiple application threads access the same persistent objects, there are three difficulties to keep in mind. One, the affected application data classes should be thread-safe. For example, the application data class should prevent the buyAt price from being returned while the background thread is calculating it. Two, the persistence manager's Multithreaded property must be true. When this property is true, the JDO implementation ensures thread safety for its internal tasks. Three, the application must prevent the normal service thread from colliding transactionally with the background thread. To prevent this, avoid transactions in the background calculation. Instead set the NontransactionalRead property to true.

Capturing the Identity String

Some business services such as EJB session beans pass application data objects by value to and from remote clients. When application data objects are passed by value, they lose their JDO identity and their connection to their persistence manager. As a result, the client cannot directly modify persistent objects but must request that the service or EJB modify the persistent objects for it. But how does the service know which object the client wants to modify?

When the service is stateful, there are a number of ways for the client to indicate to the service which object it intends. The service can keep the persistent objects viewed by the client in a Map and allow the client to refer to them by name. Or it can keep them in a List and allow the client to refer to them by index. When the service is stateless, the client's best option is to indicate the object of interest by the object's identity. For the client to have the object's identity, the service must first provide it.

When an application data object uses application identity, it is straightforward to construct a corresponding identity instance from the application data object's primary key fields. For that reason, the client can return a modified unmanaged object to the service and instruct it to make the changes in the persistent object that have been made in this unmanaged object. To follow the client's instructions, the service constructs the application identity object from the key values in the unmanaged object, finds the corresponding persistent object using the persistence manager's getObjectById method, and applies the changes found in the unmanaged object to the persistent object. Because the primary key fields are present in the application data object that uses application identity, the unmanaged object carries with it the values that the application can use to get the corresponding persistent object from JDO.

Application data classes that use datastore identity require a different strategy to carry persistent identity in their unmanaged objects because the identity value is not stored in the application data object. In this case, the application data objects must capture the identity string and store it in an unmanaged field. You may recall from Chapter 1 that the identity string is obtained from the identity object's toString method. From that point forward, the logic is nearly the same. The only difference is that the persistence manager's newObjectIdInstance method must be called to construct the identity object.

The code in Listing 7-2 shows an application data class that captures its identity string. The captureIdentityString method makes the capture and returns the identity string. Since the application cannot change a datastore identity, the code that captures the identity string assumes that it is immutable except when the persistent object is deleted. The identity string is stored in an unmanaged OIDString field. This string field must be described in the JDO metadata file as unmanaged since string fields by default are managed. Chapter 5 describes the JDO metadata and its defaults.

Listing 7-2: The Apple Class Captures the Identity String for a Datastore Identity

start example
 package com.ysoft.jdo.book.statetracker; import java.io.Serializable; import javax.jdo.InstanceCallbacks; import javax.jdo.JDOHelper; public class Apple implements InstanceCallbacks, Serializable    {    // The following is an unmanaged field to hold the identity string    private String OIDString;    public String captureIdentityString()       {       // this code assumes that each object's identity value cannot be changed       // and therefore, it does not have to be recaptured if it has already been       // captured.       if (OIDString == null)          {          Object oid = JDOHelper.getObjectId(this);          if (oid != null)             OIDString = oid.toString();          }       return OIDString;       }    // the methods required by the InstanceCallbacks interface    public void jdoPostLoad()       {       captureIdentityString();       }    public void jdoPreClear()       {       }    public void jdoPreDelete()       {       OIDString = null;       }    public void jdoPreStore()       {       captureIdentityString();       }    } 
end example

The best place to capture the identity string of an existing persistent object is in the jdoPostLoad method. The specification offers no guarantee that this method will be called. The JDO specification requires that JDO call the jdoPostLoad method after it loads the default fetch group, but it is not clear whether the JDO implementation must call the jdoPostLoad method if it opts to load the fields of the default fetch group one at a time, rather than all together. Loading the fields of the default fetch group all together is a significant optimization. For this reason, in spite of the lack of clarity in the specification, there is a high probability that serializing the object or accessing any field in the default fetch group will trigger the jdoPostLoad callback.

The captureIdentityString method is public so that the application can call it at any point. Since the method caches an already captured identity string, there is little cost to calling it more than once. When an object is made persistent and the transaction commits, the code in the jdoPreStore method captures the identity string. When an apple is serialized, the OIDString field is written to the serialization stream. In that way, it travels with the object and is available in unmanaged copies of the object. When the apple is deleted, jdoPreDelete is called and the cached identity string is set to null. Otherwise, once captured, it is immutable.

Managing Application-Generated Keys

In some cases, an application key value is defined elsewhere and simply used by the application. Governments provide identification numbers for individuals. Organizations provide UPCs for retail products and VINs for automobiles. In the near future, low-cost, universal, unique identifier buttons will be attachable to just about everything. In these cases, the key value is a fact about the person or item that is unique and discoverable.

In other cases, the application generates the key value when the object representing the person or item is created in memory or stored in the database. In these cases, the application that generates the identity usually has some business requirements for the range and sequence of key values. Sometimes, the key value is encoded with business meaning. In some cases, every key value that is consumed must be accounted for. In other cases, the requirement is only to avoid needlessly wasting keys.

Avoiding Holes in the Key Sequence

When the application is managing the key generation, it may be a requirement to avoid holes in the key sequence. For example, there should be a purchase order for every issued purchase order number. It is always difficult to audit every key value issued, since there are many reasons why the insertion into the datastore may fail.

A common approach that avoids creating holes in the key sequence, but does not eliminate them entirely, is to delay consuming a generated key for as long as possible. With JDO, each application key field must contain a valid, non-null value at the time that the persistence manager's makePersistent method is called. Likewise, the application key field must contain a valid value when the object becomes persistent because of reachability. These constraints limit how long the application can wait before assigning a value to an application key field.

The application cannot wait until the jdoPreStore method is called to assign valid keys. In fact, the object becomes persistent and JDO constructs its application identity before JDO calls the object's jdoPreStore method.

The specification allows an implementation, at its option, to support applications that change the values of key fields in persistent objects. In this case, the application may change the primary keys in jdoPreStore, but this strategy may not accomplish much since the transaction's commit could still fail. Some early implementations of JDO encounter problems in carrying out the switch in jdoPreStore correctly. It is too soon to tell whether these failures are simply implementation bugs or whether a deeper issue lies unresolved in the JDO specification.

In short, JDO does not offer a simple way to guarantee that all keys issued are in fact used, and the methods in InstanceCallbacks offer little help in this endeavor.

Avoiding a Needless Waste of Keys

If the concern is not how to avoid every hole in the generated key sequence, but rather how to make sure that a key value is generated for every object, then assigning the generated key in a constructor is a good strategy. As you may recall from the discussions in Chapter 5, JDO calls the no-arg constructor when it is time to instantiate a persistent object in memory. To avoid wasting a lot of generated key values, avoid assigning a primary key in the no-arg constructor. There is no reason to assign values to primary key fields when JDO will soon reset the fields with the values found in the database. When the application needs to construct a new and not-yet-persistent application data object, it can use another constructor to obtain an instance with an assigned key value.

Implementing Cascading Deletes

When one object is part of another object, it is related by composition to the containing object. Composition is stronger than simple aggregation because the lifetime of the contained object is bound by the lifetime of the containing object. To implement the semantics of this relationship, the application deletes the contained objects when the container is deleted. This step is usually called a cascading delete.

JDO does not directly support cascading deletes; however, in the jdoPreDelete method the application can implement cascading deletes. The jdoPreDelete method is called when the persistence manager's deletePersistent method is called. In this method, the application can ensure that any contained application data objects are also deleted by calling the deletePersistent method for them.

Listing 7-3 shows a hypothetical example of cascading deletes in the jdoPreDelete method. The example is fanciful but straightforward. When the application deletes a banana split by calling the persistence manager's deletePersistent method, JDO in turn calls back to the banana split's jdoPreDelete method. At this point, the banana split takes responsibility for deleting the persistent application data objects that compose it. Embedded objects like strings, dates, and so forth do not need to be deleted because their embedded status ensures their removal. Some application data objects, such as the ice cream store where the banana split was made, are merely associated with the banana split and are not related by composition. For this reason, the banana split does not delete the ice cream store in its jdoPreDelete method.

Listing 7-3: Deleting a Banana Split and the Objects That Compose It

start example
 package hypothetical.example; import java.util.*; import javax.jdo.*; public class BananaSplit implements InstanceCallbacks    {    // the persistent fields of a BananaSplit    // the BananaSplit is composed of these application data objects    private Banana       banana;    private IceCream     iceCream;    private Sprinkles    sprinkles;    private WhippedCream whippedCream;    private HashSet      toppings;    // a persistent reference to an embedded data object    private Date         timeCreated;    // a persistent reference to an associated application data object    private IceCreamStore sellingStore;    // the methods required by the InstanceCallbacks interface    public void jdoPostLoad()       {       }    public void jdoPreClear()       {       }    public void jdoPreDelete()       {       PersistenceManager pm = JDOHelper.getPersistenceManager(this);       pm.deletePersistent(banana);       pm.deletePersistent(iceCream);       pm.deletePersistent(sprinkles);       if (toppings != null)          {          pm.deletePersistentAll(toppings);          }       }    public void jdoPreStore()       {       }    } 
end example

In Listing 7-3, several other application data classes are mentioned for this hypothetical example. If the IceCream class were composed of persistent objects, in its jdoPreDelete method it would handle the cascading deletes of its composing objects, and so on. A contained object does not delete its container, as this would lead to incorrect semantics and infinite recursion.

In Listing 7-3, the call to the deletePersistentAll method for the toppings collection does not delete the Collection object itself; instead, it deletes the Topping objects contained in the collection. Because Collection objects are embedded objects, JDO ensures that they are deleted automatically.

Although the application can take responsibility for implementing the cascading deletes, the JDO implementation must offer some support beyond what the JDO specification requires in order for the cascading deletes to work. After the cascading deletes have occurred, the persistent objects are put in the persistent-deleted management state. Before the transaction completes the commit process, JDO sends delete statements to the datastore. If the implementation does not send the delete statements in the correct order, it is possible that the datastore may refuse to execute them. The JDO specification does not mention the need to do this, but if an implementation does not support the requirements of its datastore when flushing multiple deleted objects, the commit fails.

Some implementations go beyond simply supporting application-written cascading deletes. These implementations supply ways to specify the cascading deletes either in the JDO metadata or in supplemental XML control files.

Cooperating with JDO's Cache Management

Persistent application data objects, like any Java objects, cannot be garbage collected as long as they are reachable, by a strong reference or a chain of strong references, from an object that cannot be garbage collected. Except for the references contained in the reference classes of the java.lang.ref package, all references in Java are strong references.

When an application data object is transactional, JDO holds a strong reference to the object. JDO does not hold any reference to unmanaged objects, which are in the JDO-transient state. JDO holds a soft or weak reference to persistent application data objects that are not transactional.

The interesting thing about soft and weak references is that the garbage collector will clear the reference and collect the object before the JVM returns an OutOfMemoryError. This makes soft and weak references suited for cache management, where the desire is to hang onto the reference for as long as possible but release it when available memory is low. But the JVM can clear the soft reference and collect the hollow object only if the application is not holding a strong reference to the object. As a result, the application must cooperate with JDO to allow the garbage collector to take persistent objects that are not transactional.

Note

For more on soft and weak references, see the JDK Javadoc for the java.lang.ref package.

The application cooperates by cleaning up references to application data objects. If persistent objects are placed into event notification chains or unmanaged collections, some consideration must be given to when to remove them from the same. This might be done at the end of the transaction or at some other time. One place where it might be done is when the persistent object changes to the hollow state. When this occurs, JDO calls the jdoPreClear callback. In this callback, the application could clear strong references to the persistent object. Whether it is appropriate to do so depends on the details of the application's design.

Implications of Implementing InstanceCallbacks

When the client layer that uses application data classes is remote from the service that uses JDO, it is possible to deploy enhanced classes at the service layer and unenhanced classes at the client layer. There is no requirement to deploy unenhanced classes at the client, but if this is not done, the client layer needs access to the JDO classes contained in the javax.jdo and javax.jdo.spi packages.

The use of InstanceCallbacks significantly complicates the logistics of deploying a free-from-JDO version of the application data classes because the preenhanced application data class now contains a reference to the InstanceCallbacks interface. In this situation, it is best to just deploy enhanced classes on both the client and service layers. Any other approach is not likely to produce a return commensurate with the effort and risk. Although the client receives no benefit from the code contained in the JDO specification packages, it is not a lot of code to deploy, and it will not change often.

Throwing Unchecked Exceptions Within Callback Methods

The application code that implements the InstanceCallbacks interface cannot throw any checked exceptions, because the interface signatures do not declare any checked exceptions. Because exceptions of type RuntimeException are not checked, the application code can throw a RuntimeException and any exception, such as JDOException, that derives from it.

Within the callback method, the application might throw a runtime exception intentionally to signal a failure condition or unintentionally as a result of a programming error. Or, it might call JDO from within the callback, and this call may cause a runtime exception to be thrown.

Once an unchecked exception has been thrown in the callback method, what happens to it? The JDO specification does not address this question, and the answer depends on the implementation. Most implementations are likely to allow unchecked exceptions to percolate up to the code that called JDO. This code may have been written by the developer or added during enhancement to the application data class. In short, the application must handle the unchecked exceptions thrown from callback methods in the same way as it handles all exceptions thrown by the JDO implementation.




Using and Understanding Java Data Objects
Using and Understanding Java Data Objects
ISBN: 1590590430
EAN: 2147483647
Year: 2005
Pages: 156
Authors: David Ezzio

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net