6. Object Caching
Contents
6.1. Introduction
6.2. Using Caches
6.3. Automatic Caching
6.1. Introduction
This section discusses issues related to the persistence layer caching feature. The caching behavior differs in each edition of VBSF. The Enterprise Edition features a global shared cache, while the Professional Edition does not.
The Enterprise Edition global shared cache is maintained in the Server object and shared by all Database objects managed by the Server. All operations performed in one Database object are automatically available in all other Database objects managed by the same Server once the transaction commits. All synchronization occurs in memory without incurring any database hits. Changes to objects performed on one Database object will not be visible to other Database objects until the transaction is committed in the database.
In addition to the shared cache, in the Enterprise Edition each Database object also maintains a transactional cache. There is currently no concept of read-only-threads in VBSF, so all threads are assumed to be read/write threads. As a result, each transactional cache maintains transactional copies of all persistent objects to allow each thread to make changes to the objects without interfering with other threads. Internally, all transactional persistent objects from multiple Database objects corresponding to the same persistent object in the database synchronize to the same shared proxy in the global shared cache.
In the Professional Edition each Database object that is in use by a client maintains its own object cache and, as a result, the cache is session specific to each client or thread for the duration of the session. Changes performed in one Database object do not update the caches in other active Database objects once the transaction commits.
When retrieving objects from the persistence layer you have the choice of storing the objects in a cache after they are retrieved from the database. The remaining sections discuss how to configure, and how to enable or inhibit caching, and the tradeoffs involved.
6.2. Using Caches
The Database, OCollection
, and OReference classes provide two types of
methods to retrieve objects: get() and list(). Usage of the cache is dependent on which of the
two methods is used:
To enable caching invoke only get() methods when
retrieving objects. To inhibit caching invoke list()
methods.
VBSF caches are built incrementally. Only the objects retrieved are stored in the cache. Once objects are cached in memory further requests for the same objects are retrieved from the cache. This rule is simple to follow when requesting an object by its object ID because either the object is in the cache or is not. Retrieving objects that match a query specification from a cache (i.e. enabling in-memory queries) is more complicated. Unless the cache is loaded in full, the persistence layer has no way of knowing that the search criteria can be fully satisfied from the cache. This forces all queries to be executed from the database even when using get() methods. The only way to enable in-memory queries is to invoke a method that loads the cache in full prior to performing queries. Both the load() method and the get() method that takes no arguments load the cache in full from the database. Since in-memory queries dramatically improve query speed, they should be enabled whenever a collection is going to be queried multiple times within a session and as long as the collection is not so large that loading it in full could cause resource problems.
6.3. Automatic Caching
Even if your application does not use the cache by using exclusively list() methods, VBSF still caches individual objects when they are passed back as argument to a Database, OCollection, or OReference method, if the objects are not already in the cache. This automatic caching is necessary in order for VBSF to be able to maintain the integrity of relationships during edit sessions, and so VBSF can be smart about subsequent updates to these objects (e.g., ignore an update request if no changes have been to the object).
Automatic caching of an object happens regardless of whether the object was originally retrieved using a get() or list() method. For example, after execution of the following code sequence, the Contact object with object ID = 7 will be cached, eventhough the object was originally retrieved using a list() method:
Contact currContact = (Contact)db.list(Contact.class, 7L);
currContact.setLastName("STALL");
db.update(currContact);
There are circumstances, such as when caching interferes with changes performed to the database outside of VBSF, when it might be desirable to remove these objects from the cache. In this case, you may explicitly remove objects from the cache, as explained in the following section, or have VBSF automatically empty the cache at the end of every transaction, regardless of whether the transaction completed successfully or was aborted. This feature can be enabled by checking the Automatic discard of cache option in the Transactions Panel of the mapping tool DB Configuration form. In the Enterprise Edition this option applies only to the transactional cache. The Global shared cache is not affected. In general, this option is not recommended in the Enterprise Edition.
Note that when a Database object is closed to return it back to the Database pool maintained by a Server VBSF automatically clears the transactional cache of the Database object before returning it the pool.
VBSF supports two types of caches: static and dynamic. In Static caches, once an object is stored in the cache it is never garbage collected. However, objects may be explicitly removed from the cache by performing a discard operation. Static caches may be used when it is undesirable to remove objects from the cache. This is useful for classes that do not have a large extent and represent categorical or look-up information that is often used throughout the application.
In Dynamic caches, objects can be automatically garbage collected from the cache by the JVM if it's necessary to free up memory. There are two types of dynamic caches:
Both Static and Dynamic caches can be optionally managed by VBSF using an MRU (most recently used) algorithm. If a Dynamic cache is not managed, cache entries are removed from the cache solely at the discretion of the garbage collector according to the JVM policy for soft or weak references. If the cache is managed, VBSF assures that the cache maintains a certain predefined size. If the cache is dynamic, the size specifies the minimum number of entries to maintain in the cache. More specifically, the size is the number of most recently used entries guaranteed to be in the cache. If the cache is static, the size specifies the maximum size that the cache can grow to. The cache can be configured on a per persistent class basis. For details on how to configure the cache, see Cache Configuration in section 4.1 of the Mapping Tool Guide.
The VBSF API provides a set of discard operations that can be used to explicitly release memory in both Static and Dynamic caches. Discarding the cache releases all the memory used by the cache, so the garbage collector can reclaim it during its sweep. Both the Database and the OCollection class provide two basic methods to release memory:
In addition, the Database class provides a discardAll() method that can be used to clear out all transactional caches in one call. Note there should be no need to explicitly call this method as it is automatically called when a Database object is closed to return it back to the pool.
Below is an example on the use of the discard(object) method. The example removes from the cache an object that was automatically cached by the persistence layer when it was inserted:
Customer customer = new Customer();
customer.setName("Jean Luc Picard");
db.update(customer); //customer is now
cached
db.discard(customer); //customer removed from
cache
6.5. Pitfalls of list() Methods
Some applications might want to disable caching altogether. These types of applications will typically use only list() methods, and discard all objects that were automatically cached by VBSF. However, blindly following these rules could result in considerable performance degradation. Let's take the code below, taken from the vcontactdemo3.engine2 sample application, as an example of typical code that could be written in this type of application:
AddressBook currBook =
(AddressBook)db.list(AddressBook.class, 1L);
Contact[] allContacts = (Contact[])db.list(currBook, "contacts");
This code retrieves the AddressBook with ID = 1 from
the database without caching it, and then retrieves all its Contact
objects, again without caching them. The problem with this code is that when you pass the AddressBook object to the list
method in the second line, VBSF retrieves it from the database again. This is because
because VBSF actually caches proxies of your business objects, and these proxies maintain
their relationships internally. VBSF can only traverse relationships via its internal
proxies. Since VBSF does not find the proxy for the AddressBook
object with ID = 1 in the cache, it must first build the proxy by retrieving the object
from the database again. Note that VBSF cannot simply build the proxy from the actual
values of the currBook object because it needs to
know the current values in the database for concurrency control purposes. Once the proxy
of currBook is in the cache, VBSF can traverse the
relationship to its Contact objects. The end result
is that after executing the above two line of code the same query against the AddressBook table is executed twice.
If, on the other hand, we cache the AddressBook
object when we retrieve it, then the second statement does not result in an unnecessary
database hit. The AddressBook object can then be
explicitly discarded after the Contact objects are
retrieved. The revised code is shown below:
AddressBook currBook =
(AddressBook)db.lookup(AddressBook.class, 1L);
Contact[] allContacts = (Contact[])db.list(currBook, "contacts");
db.discard(currBook);
This strategy should be applied on as many traversal levels as necessary. For example,
if we now want to get the Address objects for each Contact, and we simply add some code, leaving the existing
code unchanged as shown below, we will run into the same problem again:
AddressBook currBook =
(AddressBook)db.lookup(AddressBook.class, 1L);
Contact[] allContacts = (Contact[])db.list(currBook, "contacts");
for (int i=0; i < allContacts.length; i++) {
Address allAdr = (Address[])db.list(allContacts[i],
"currentAddresses");
}
db.discard(currBook);
Now each time a Contact object is passed to the list method inside the loop, it must be retrieved from the
database again because it is not in the cache anymore. Below is the same code revised one
more time to avoid the problem:
AddressBook currBook =
(AddressBook)db.lookup(AddressBook.class, 1L);
Contact[] allContacts = (Contact[])db.get(currBook, "contacts");
for (int i=0; i < allContacts.length; i++) {
Address allAdr = (Address[])db.list(allContacts[i],
"currentAddresses");
}
db.discard(currBook);
Note that we did not explicitly have to discard the Contact
objects because when the AddressBook object is
discarded all its owned collections are automatically discarded. Also note that we also
used a list method to retrieve the Address objects. This is fine if no changes are to be done
to the Address object that have to be written back
to the database. However, if the Address objects can
be updated, then you should use a get method to
retrieve them. This is because VBSF will cache every Address
object that is passed back to the Database.update()
method, resulting in another database hit just to retrieve the object in order to cache
it.
In general, when retrieving objects for update sessions it is best to use only get methods so VBSF can build the cache of relationships internally, and turn on the option to automatically discard the cache after every transaction. That way after all updates are done, the cache will be automatically emptied.
When writing code that deals with these issues it is usually a good idea to turn on debugging so you can see whether unexpected database hits are occurring, and if so, adjust your code to avoid them.