JDO lifecycle listeners
JDO 2.0 defines an interface for the PersistenceManager (and
PersistenceManagerFactory) that allows a listener to be registered for
persistence events in the “lifecycle” of a data object—creating, deleting, loading,
or storing the object. Once a listener is set up for a PersistenceManager, the JDO
implementation will call methods on the listener when specified persistence events
occur. This allows the application to define methods that monitor the persistence
process (for specified persistent classes). We will use this feature to support object
cache management in Connectr.
To set up a JDO lifecycle listener, create a class that implements any number of
the following javax.jdo.listener interfaces—DeleteLifecycleListener,
CreateLifecycleListener, LoadLifecycleListener, and
StoreLifecycleListener. Each interface requires that the implementing class
implements a “pre” and/or “post” method for the associated event type. These
methods, often referred to as callbacks, are called before and after the persistence
event. For example, preDelete() and postDelete() method s are defined for the
DeleteLifecycleListenerinterface, which are called before and after a delete event.
Each method takes as its argument the InstanceLifecycleEvent event that
triggered it. If triggered within a transaction, the listener actions are performed
within the context of that same transaction.
See http://www.datanucleus.org/products/
accessplatform_1_1/jdo/lifecycle_listeners.
html for further details on Lifecycle listeners.
The following code shows one of the listener classes used in Connectr, server.
utils.cache.CacheMgmtTxnLifecycleListener. It implements
the DeleteLifecycleListener and LoadLifecycleListener interfaces,
and thus is required to define methods called on delete and load events.
import javax.jdo.listener.DeleteLifecycleListener;
import javax.jdo.listener.InstanceLifecycleEvent;
import javax.jdo.listener.LoadLifecycleListener;
public class CacheMgmtTxnLifecycleListener implements
DeleteLifecycleListener, LoadLifecycleListener {
public void preDelete(InstanceLifecycleEvent event) {
removeFromCache(event);
}
public void postDelete(InstanceLifecycleEvent event) {
// must be defined even though not used
}
public void postLoad(InstanceLifecycleEvent event) {
removeFromCache(event);
}
private void removeFromCache(InstanceLifecycleEvent event) {
Object o = event.getSource();
if(o instanceof Cacheable) {
Cacheable f = (Cacheable) o;
f.removeFromCache();
}
}
}
Once a listener class has been defined, you can associate it with a given
PersistenceManager instance as follows:
pm.addInstanceLifecycleListener(
new CacheMgmtTxnLifecycleListener(), classes);
where classes is an array of java.lang.Class objects for which changes should be
listened for. That is, the PersistenceManager will listen for delete and load events on
the given classes and call the defined listener methods when they occur.
Defining a cacheable interface
To use the JDO listeners to support Memcache management in Connectr, we’ll first
define a server.utils.cache.Cacheable interface:
public interface Cacheable
{
public void addToCache();
public void removeFromCache();
}
Any data class that implements this interface must define two instance
methods—an addToCache() method , which adds that object to Memcache, and
a removeFromCache() method , which deletes that object from Memcache. The
lifecycle listener methods (as in the previous example, server.utils.cache.
CacheMgmtTxnLifecycleListener) can thus safely call addToCache() and
removeFromCache() on any Cacheable object.
The implementations of these methods will in turn use the server.utils.cache.
CacheSupport class (described previously) to actually access the cache, using the
class name to set the Memcache namespace.
In Connectr, both the Friend and FeedInfo classes implement Cacheable.
The following code shows the server.domain.FeedInfo implementation of
addToCache()and removeFromCache() in support of that interface. For FeedInfo,
the call to getFeedInfo() prior to adding the object to Memcache forces the feed
content field to be fetched. Otherwise (because it is lazily loaded), that field would
not be included when the object was serialized for Memcache.
@PersistenceCapable(identityType = IdentityType.APPLICATION,
detachable="true")
public class FeedInfo implements Serializable, Cacheable {
…
@Persistent
@PrimaryKey
private String urlstring;
…
private int update_mins;
public void removeFromCache() {
CacheSupport.cacheDelete(this.getClass().getName(), urlstring);
}
public void addToCache() {
getFeedInfo();
CacheSupport.cachePut(this.getClass().getName(),urlstring,this);
}
…
}
The Connectr application’s lifecycle listeners
Any number of lifecycle listener classes can be defined, which implement handlers
for some or all of the persistence events. For Connectr, we have defined two—one
for the case where the PersistenceManager is being used to manage a transaction,
and one for the case where there is non-transactional Datastore access. This simple
distinction works for our application; more complex applications might use others.
As discussed earlier, with transactions, it is safest to conservatively remove cached
information for any object that the transaction accesses. In that way, if the transaction
does not commit, the cac he will not be out of sync. The server.utils.cache.
CacheMgmtTxnLifecycleListener class listed previously will be used for accesses
inside transactions, and deletes the object from the cache after the object is loaded or
before it is deleted from the Datastore. It does not add to the cache at any point.
If not operating within a transaction— for example, for a read—then the server.
utils.cache.CacheMgmtLifecycleListener class, which follows, is used. It too
deletes an object from the cache before it is deleted in the Datastore. However, this
Listener caches an object after it is loaded or stored.
public class CacheMgmtLifecycleListener implements
DeleteLifecycleListener, LoadLifecycleListener,
StoreLifecycleListener {
public void preDelete(InstanceLifecycleEvent event) {
Object o = event.getSource();
If (o instanceof Cacheable) {
Cacheable f = (Cacheable) o;
f.removeFromCache();
}
}
public void postDelete(InstanceLifecycleEvent event) {
// must be defined even though not used
}
public void postLoad(InstanceLifecycleEvent event) {
addToCache(event);
}
public void preStore(InstanceLifecycleEvent event) {
}
public void postStore(InstanceLifecycleEventevent) {
addToCache(event);
}
private void addToCache(InstanceLifecycleEvent event) {
Object o = event.getSource();
if (o instanceof Cacheable) {
Cacheable f = (Cacheable) o;
f.addToCache();
}
}
}
Using the lifecycle listeners consistently
In Connectr, we will use Memcache to cache objects of the FeedInfo and Friend
(and child) data classes. In later chapters, as we further develop the app, we will
make additional use of the cache as well. We can use our two JDO lifecycle listeners
to impose consistency on our object caching: we can enforce the use of one of the
two listeners with each PersistenceManager and set the listeners to apply only
to the FeedInfo and Friend classes, so that the listeners are not being employed
unnecessarily.
To fac ilitate this, we will modify the app’s server.PMF singleton class (shown as
follows) to define the list of data classes for which the app uses Listener-based
caching. Then, we’ll add two methods (getTxnPm() and getNonTxnPm()) that return
a PersistenceManager with the appropriate lifecycle listener set. Now, instead
of simply grabbing a generic PersistenceManager, we will obtain a configured
one via getTxnPm() or getNonTxnPm(), depending upon whether or not the
PersistenceManager will be used inside a transaction.
The following code shows the modified server.PMF class (in the classes array,
replace ‘packagepath’ with your path).
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.PersistenceManager;
public final class PMF {
private static final java.lang.Class[] classes =
new java.lang.Class[]{
packagepath.server.domain.FeedInfo.class,
packagepath.server.domain.Friend.class};
private static final PersistenceManagerFactory pmfInstance =
JDOHelper.getPersistenceManagerFactory("transactions-optional");
privatePMF(){
}
public static PersistenceManagerFactory get(){
return pmfInstance;
}
public static PersistenceManager getNonTxnPm(){
PersistenceManager pm = pmfInstance.getPersistenceManager();
pm.addInstanceLifecycleListener(new CacheMgmtLifecycleListener(),
classes);
return pm;
}
public static PersistenceManager getTxnPm(){
PersistenceManager pm = pmfInstance.getPersistenceManager();
pm.addInstanceLifecycleListener(
new CacheMgmtTxnLifecycleListener(), classes);
return pm;
}
}
Checking for a cache hit
Once the Listener-based mechanism is in place to add objects to the cache
appropriately, we can check the cache on reads, given the ID of an object. If we get
a cache hit, we use the cached copy of the object. If we have a cache miss, we simply
load the object from the Datastore. Using the CacheMgmtLifecycleListener, a
Datastore load invokes the postLoad() Listener method. This will cache the object,
making it available for subsequent reads.
The following code illustrates this approach in the getFriendViaCache() method of
server.FriendsServiceImpl.
@SuppressWarnings("serial")
public class FriendsServiceImpl extends RemoteServiceServlet
implements FriendsService
{
…
private Friend getFriendViaCache(String id, PersistenceManager pm){
Friend dsFriend = null, detached = null;
// check cache first
Object o = null;
o = CacheSupport.cacheGet(Friend.class.getName(), id);
if (o != null && o instanceof Friend) {
detached = (Friend) o;
}
else {
// the fetch will automatically add to cache via the lifecycle
// listener
dsFriend = pm.getObjectById(Friend.class, id);
dsFriend.getDetails();
detached = pm.detachCopy(dsFriend);
}
return detached;
}
…
}
Memcache is not useful solely for data object caching. It is often used to cache
display-related information as well. We will see this usage of Memcache in a
later chapter.
Summary
In this chapter, we’ve explored ways in which you can make a GAE application more
robust, responsive, and scalable in its interactions with the Datastore, and applied
these techniques to our Connectr app.
We first took a look at how Datastore access configuration and data modeling can
impact application efficiency and discussed some approaches to entity design
towards scalability.
Then, we introduced transactions—what they are, the constraints on a transaction in
App Engine, and when to use them.
Finally, we discussed the App Engine’s Memcache service, how caching can speed
up your app’s server-side operations, and looked at an approach to integrating
caching with access to “lifecycle events” on JDO data objects.
GWT Articles & Books
- Google Web ToolKit(GWT)
- GWT Books
- GWT Official Site