[ Team LiB ] |
14.6 Modifying Persistent Instances Outside a TransactionJDO manages updates to the datastore by tracking changes made to persistent instances during a transaction. To avoid losing updates, you should have an active transaction when changing fields of persistent instances. When the transaction commits, the changes are made in the datastore. However, you can write applications that manage a cache of nontransactional persistent instances, where the datastore is updated outside your application. With these applications, the cache becomes stale relative to the current state in the datastore. But if your application is made aware of these changes—for example, by receiving a stream of change notifications—your application can update the cache to reflect the current state of the datastore instances. The stream might consist only of the keys of the instances, in which case the application can simply invalidate the cached instances by calling evict( ) or refresh( ). But if the stream contains not only the keys but also the changed values for persistent fields, your application can use the stream values to update the cached instances to reflect the current contents of the datastore. With the NontransactionalWrite property set to false, the only way to update nontransactional instances is to invalidate them in the cache and then fetch the instances from the datastore when they are next needed. But with NontransactionalWrite set to true, your application can update the persistent instances in the cache without beginning a transaction and updating the instances. Your application can make updates to any values, but the most useful approach updates the values in the cache to reflect the current values in the datastore. Note that the values of fields in persistent-nontransactional instances that have been modified outside a transaction will never be stored in the datastore by the JDO implementation. Any changes made outside of a transaction are lost. This is due to the behavior of transactional instances. In a subsequent datastore transaction, if the instance is accessed (by field access, extent iteration, query, or navigation), a fresh copy of the instance will be fetched into the cache and the values written outside the transaction will simply be discarded without notice. With NontransactionalWrite set to false, if your application attempts to make a change to any persistent instance outside a transaction, the JDO implementation will throw a JDOUserException. This includes executing any method that changes a field in the instance and executing JDOHelper.makeDirty( ) , referencing a field of any persistent instance. 14.6.1 Hot Cache ExampleFor example, consider an application that executes in multiple JVMs, each of which manages a hot cache of Movie instances that track changes to a Movie's web site via a live feed. One of the JVMs executes MasterDriver, the application responsible for updating the datastore; the others execute SlaveDriver, an application that updates its copy of the instances in its cache when updates arrive. Both MasterDriver and SlaveDriver extend AbstractDriver. The constructor of AbstractDriver connects to the source of cache updates and cache requests. We open the request and update input streams from a URL, which might be a file, or in a more realistic application, a stream from an external source. The results of a request are output to System.out, which is not realistic but demonstrates the concept: public class AbstractDriver { protected BufferedReader requestReader; protected BufferedReader updateReader; protected CacheAccess cache; protected int timeoutMillis; protected AbstractDriver(String updateURL, String requestURL, String timeout) { updateReader = openReader(updateURL); requestReader = openReader(requestURL); timeoutMillis = Integer.parseInt(timeout); } The BufferedReader allows us to read lines from the input source: protected BufferedReader openReader (String urlName) { try { URL url = new URL(urlName); InputStream is = url.openStream( ); Reader r = new InputStreamReader(is); return new BufferedReader(r); } catch (Exception ex) { return null; } } ServiceReaders will service the updateReader and requestReader until there is no work to do for a specified timeout period, or until it is interrupted: protected void serviceReaders( ) { boolean done = false; boolean lastTime = false; try { while (!done) { if (updateReader.ready( )) { handleUpdate( ); done = false; lastTime = false; } else if (requestReader.ready( )) { handleRequest( ); done = false; lastTime = false; } else { try { Thread.sleep (timeoutMillis); if (lastTime) done = true; lastTime = true; } catch (InterruptedException ex) { done = true; } } } } catch (Exception ex) { return; } } HandleRequest reads a line from the requestReader and prints the title of the movie to System.out. A more realistic application would return the results to the requester. protected void handleRequest( ) throws IOException { String request = requestReader.readLine( ); Movie movie = cache.getMovieByTitle(request); System.out.println("Movie: " + movie.getTitle( )); } HandleUpdate reads a line from the updateReader, parses it into a movie title and a web site, and then calls updateWebSite. protected void handleUpdate( ) throws IOException { String update = updateReader.readLine( ); StringTokenizer tokenizer = new StringTokenizer(update, ";"); String movieName = tokenizer.nextToken( ); String webSite = tokenizer.nextToken( ); cache.updateWebSite (movieName, webSite); } } The interface to the cache is defined by com.mediamania.hotcache.CacheAccess. There are two implementations of this interface: MasterCache and SlaveCache, with a common AbstractCache implementation. MasterCache performs the updates to the datastore as well as updating the cache. It will retrieve the Movie into the cache if it is not already cached. SlaveCache updates the cache only if the Movie is already cached. MasterCache needs the NontransactionalRead option set to true because lookups are done outside a transaction, and the RetainValues option set to true so values are retained in the cache at the end of an update transaction. SlaveCache needs the NontransactionalRead and NontransactionalWrite options set to true, because reads and updates are done without a transaction active. Both classes use getPropertyOverrides( ) to initialize the PersistenceManagerFactory with the correct options. AbstractCache implements the CacheAccess interface: public interface CacheAccess { Movie getMovieByTitle (String title); void updateWebSite (String title, String website); } MasterCache and SlaveCache use the same lookup method implemented in AbstractCache to find a Movie with a particular title. If the Movie does not exist in the cache, it is loaded (outside a transaction) into the cache. public abstract class AbstractCache extends MediaManiaApp implements com.mediamania.hotcache.CacheAccess { protected Map cache; // key:name value:Movie public Movie getMovieByTitle(String title) { Movie movie = (Movie)cache.get(title); if (movie == null) { movie = super.getMovie(title); if (movie != null) { cache.put(title, movie); } return movie; } } The difference between MasterCache and SlaveCache is in how the update is handled. MasterCache first loads the Movie into the cache if it isn't already there, and then uses a transaction to perform the update: public class MasterCache extends AbstractCache implements CacheAccess { protected static Map getPropertyOverrides( ) { Map overrides = new HashMap( ); overrides.put ("javax.jdo.options.NontransactionalRead", "true"); overrides.put ("javax.jdo.options.RetainValues", "true"); return overrides; } public void updateWebSite(String title, String website) { Movie movie = getMovieByTitle(title); if (movie != null) { tx.begin( ); movie.setWebSite(website); tx.commit( ); } } } SlaveCache locates the movie in the cache. If the Movie is not in the cache, SlaveCache ignores the message. If the Movie is in the cache, SlaveCache updates it: public class SlaveCache extends AbstractCache implements CacheAccess { protected static Map getPropertyOverrides( ) { Map overrides = new HashMap( ); overrides.put ("javax.jdo.options.NontransactionalRead", "true"); overrides.put ("javax.jdo.options.NontransactionalWrite", "true"); return overrides; } public void updateWebSite(String title, String website) { Movie movie = (Movie)cache.get(title); if (movie != null) { movie.setWebSite(website); } } } To complete the example, MasterDriver initializes the cache to be a MasterCache: public class MasterDriver extends AbstractDriver { protected MasterDriver(String updateURL, String requestURL, String timeout) { super(updateURL, requestURL, timeout); cache = new MasterCache( ); } public static void main(String[] args) { MasterDriver master = new MasterDriver( args[0], args[1], args[2]); master.serviceReaders( ); } } SlaveDriver initializes the cache to be a SlaveCache; otherwise, the implementation is the same as MasterDriver: public class SlaveDriver extends AbstractDriver { protected SlaveDriver(String updateURL, String requestURL, String timeout) { super(updateURL, requestURL, timeout); cache = new SlaveCache( ); } public static void main(String[] args) { SlaveDriver slave = new SlaveDriver( args[0], args[1], args[2]); slave.serviceReaders( ); } } |
[ Team LiB ] |