Each persistence manager controls a cache of persistent objects. Each persistent object has persistent fields that hold the values that JDO can synchronize with the datastore. When the application first gets the value of a persistent field in a persistent object, the persistence manager loads the value from the datastore. The cost to fetch the value of a persistent field from the datastore is never paid more than once per transaction.
The persistence manager uses the application data object as the primary place to store the values of its persistent fields. When a persistent field does not contain its persistent value, JDO sets the field to its Java default value. When a persistent object has no persistent state in memory, it is in the hollow management state. Since the persistent value may be the same as the Java default value, JDO must always know whether the persistent field holds the field's persistent value or its default value.
JDO may read a field's value from the datastore and cache it prior to the application's access to the field. The JDO implementation reads the default fetch group in one database access as a performance optimization. The default fetch group is a block of fields that are usually stored together in the datastore. The JDO metadata defines, either explicitly or by default, which fields are members of the default fetch group. Chapter 5 discusses the JDO metadata.
As a general observation, the performance benefit of caching is the discounted cost of subsequent use of cached information. To gain the benefit from JDO, the application must access the value of the persistent field more than once, and it must do so before JDO discards the persistent field's value.
The JDO specification describes caching for one persistence manager. The JDO implementation may architect a deeper caching strategy that provides for caching across all of the persistence managers in one or more JVMs. JDO does not specify whether the implementation does this, or how it does this. The section entitled "The JDO Implementation's Second-Level Cache," found later in this chapter, discusses the deeper caching strategy. Otherwise, the caching architecture discussed here is the one-cache-per-persistence-manager architecture that JDO specifies.
The garbage collector can remove some persistent objects from the persistence manager's cache. JDO usually holds a strong reference to all transactional objects, but it holds only a weak or soft reference to objects that are either hollow or persistent-nontransactional. As a result, unless the application holds a strong reference to a hollow or persistent-nontransactional object, the object can be garbage collected.
The java.lang.ref package defines the basic mechanisms of weak and soft references.
The contents of the persistence manager's cache has little effect on the performance of queries made against an extent. The reason is straightforward: queries against an extent involve a trip to the datastore to determine, at a minimum, the identity values of the objects that satisfy the query. This is the major cost of the query, and it must be paid whether the objects that the query finds are in the cache or not.
Likewise, the cache can offer only a marginal performance improvement when the application uses the objects returned by the query. The reason is not quite so straightforward as before. When JDO sends the query to the datastore to retrieve the identity values, it can usually add, at a low marginal cost to the query performance, the request to retrieve the values of the default fetch group associated with the objects found. Since the cost is low and the decision to request the default fetch group must be made before it can be known whether the objects are in the cache, the JDO implementation will usually make the request to retrieve the default fetch group. As a result, finding the object in the cache can improve performance significantly only when the application reuses a field that is not in the default fetch group.
If the persistence manager cache does not help when executing a query or iterating its results, when does it boost performance? Each of the objects that the application gets by iterating the query's results or the extent is a root of a potential graph of objects that can be reached by navigating the persistent fields that refer to other data objects. When these other objects are in memory and have most of their persistent state loaded, then JDO can dramatically improve the speed of transparent navigation by avoiding the trip to the datastore. In short, the persistence manager's cache can improve the performance of transparent navigation, and it might improve somewhat the use of the objects returned in the query results, but it cannot speed up queries.
When all five transactional properties are false, JDO limits the lifetime of persistent state to the lifetime of the transaction. When the transaction ends, the persistent object becomes either hollow or JDO-transient. The persistent state outlives a transaction only when one or more of the transactional properties are turned on. Turning on NontransactionalRead, Optimistic, RetainValues, and in some cases RestoreValues creates persistent-nontransactional objects that retain their persistent state outside of and across transactions.
By limiting caching to the transaction, the application pays a minimal amount for caching and its benefits. There is no risk of stale information since the persistent state loaded within the transaction is subject to the same transactional properties regardless of how often it is used. At the same time, when caching is limited to the transaction, the performance benefit from reuse is also limited to the transaction. For some applications, reuse within the transaction is significant, but for other applications, the significant reuse is across transactional boundaries or even across persistence managers.
When caching crosses the transactional boundaries, the application runs the risk of using potentially stale information that is no longer consistent with the state stored in the datastore. Optimistic transactions remove the risk of using outdated information for any objects that are changed or otherwise made transactional, but the risk remains for those objects that are not transactional.
As mentioned earlier, JDO does not specify that implementations provide any caching beyond the cache of persistent objects managed by each persistence manager. Nonetheless, many JDO implementations provide a second-level cache because the design patterns for using JDO often require that a persistence manager be dedicated to each request handled by a stateless service or to each user handled by a stateful service. Due to the frequently observed commonality of interests in multiple requests from the same and different users, a second-level cache can provide caching benefits when a persistence manager's cache cannot.
Chapter 9 examines the design issues in client/server applications. Chapter 10 examines the design issues in Web applications, and Chapter 11 examines the design issues when using JDO within Enterprise JavaBeans. Both Web components and EJB components are expected to make heavy use of the one-persistence-manager-perrequest design pattern. For this reason, these application architectures benefit from a second-level cache.
When the implementation provides a second-level cache, what does it mean to say that the persistent state is loaded from the datastore? The answer depends on whether the application is using a datastore transaction or an optimistic transaction. In a datastore transaction, the persistent state that is loaded must be consistent with the state available from the database transaction. Although the application may have a choice of mechanisms to guarantee consistency with the database (for example, locking the entire database, locking the schema, receiving an administrative assurance of exclusive ownership, etc.), one typical way to make this guarantee is to fetch the persistent state from the datastore within the database transaction. In this case, there is no benefit derived from state that is stored in memory prior to the start of the datastore transaction.
On the other hand, the optimistic transaction makes only some objects transactional, and it verifies the persistent state of transactional objects only on transaction commit. For these reasons, when an optimistic transaction "loads the persistent state from the datastore," it can cheat by fetching the values from a second-level cache if there is one. Later, when changes are written to the datastore upon transaction commit, the concurrency values of transactional objects are checked against the values in the datastore. By using the secondary cache, the JDO implementation can provide a performance boost for optimistic transactions that it may not be able to provide for datastore transactions.
When the application wishes to take advantage of the second-level cache provided by the JDO implementation, it should, as a general rule, use optimistic rather than datastore transactions. As JDO implementations evolve their cache management, this general rule will develop qualifications and exceptions, but it is a good place to start when seeking to understand how to benefit from the implementation's second-level cache.