Draft:How To Cache Data
Contents |
Overview
Storing data in memory, known as data caching, increases application performance by providing a less expensive means of feeding data into the application logic and by reducing the number of queries executed by the database, saving those resources for other activities.
However, using data a cache introduces the risk of executing business logic with stale data. This can result in errors or unexpected application behaviour. To prevent this, the cache must be invalidated whenever the underlying data changes.
Because of this invalidations, frequently modified data is not a good candidate for storage in the application cache. Consider carefully all the use cases that could case invalidations before adding a new data cache.
![]() | Starting from 3.0PR20Q2, the CacheManagement infrastructure is available for the implementation of data caches |
How the CacheManagement Works
The CacheManagement implementation provides two main benefits:
- An easy and standardized way of storing data in memory, for almost any data type
- A reliable process to invalidate data caches across several application servers
To control when a data cache needs to be invalidated, a new table AD_Cache stores a single record for each registered cache and holds the timestamp of the last modification of the underlying data in the database.
A background thread, implemented by CacheInvalidationBackgroundManager queries this table at a configurable frequency and informs the individual CacheManager instances if the cache they are holding needs to be refreshed.
How to Implement a New Data Cache
For the sake of this explanation, let's consider the case of the organizational structure of the clients in Openbravo. The organization tree is used very frequently in the application logic to determine the data that should be visible in a given context. On the other hand, the organizational structure changes very seldom, That makes it a good candidate for a cache.
The steps involved in implementing the cache are:
- Register the cache in the application dictionary
- Implement the CacheManager that will hold the cached data
- Create the database trigger(s) to inform the CacheManager when data needs to be refreshed
Registering the cache in the application dictionary
The first step in implementing this new cache is registering it in the Cache Invalidation Control window
The record's Search Key will work as the link between the invalidation record and the CacheManager implementing the cache.
The module in which the cache is implemented needs to be in development. More about this is explained in the Modularity page.
Implementing the CacheManager
The second step is to implement the class that will hold the cache, extending the CacheManager class.
public class OSPCacheManager extends CacheManager<String, Map<String, OrganizationStructureProvider.OrgNode>> { private static final String CACHE_IDENTIFIER = "OrganizationStructure"; @Override public String getCacheIdentifier() { return CACHE_IDENTIFIER; } }
In this case, for each key (the client id), the cache will hold a Map that contains the nodes in the organizational structure of the client.
The cache identifier, returned by the getCacheIdentifier must return the same value used in the Search Key in the previous step.
Cache Manager descendants are automatically injected by Weld to the CacheInvalidationBackgroundManager.
Creating the database trigger that notifies the need to invalidate
Whenever the organizational structure changes, this database trigger must update the last_invalidation field in the AD_Cache table. This will make the CacheInvalidationBackgroundManager request the appropriate cache to invalidate its records.
CREATE TRIGGER ad_org_cache_trg AFTER INSERT OR UPDATE OR DELETE ON ad_org FOR EACH ROW EXECUTE PROCEDURE ad_org_cache_trg(); CREATE OR REPLACE FUNCTION public.ad_org_cache_trg() RETURNS TRIGGER LANGUAGE plpgsql AS $function$ BEGIN IF AD_isTriggerEnabled()='N' THEN IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF; END IF; IF TG_OP = 'DELETE' OR TG_OP = 'INSERT' OR coalesce(new.ad_businessunit_org_id,'NULL') != coalesce(old.ad_businessunit_org_id,'NULL') OR coalesce(new.ad_calendarowner_org_id,'NULL') != coalesce(old.ad_calendarowner_org_id,'NULL') OR coalesce(new.ad_legalentity_org_id,'NULL') != coalesce(old.ad_legalentity_org_id,'NULL') OR coalesce(new.ad_periodcontrolallowed_org_id,'NULL') != coalesce(old.ad_periodcontrolallowed_org_id,'NULL') THEN UPDATE ad_cache SET last_invalidation = now() WHERE value = 'OrganizationStructure'; END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF; END; $function$;
In most scenarios, any modification to a database table should invalidate the cache. However, the trigger can check for any specific conditions before forcing cache data to be refreshed. In this case, for example, the cache is only invalidated when the fields controlling the organizational structure are updated.
Additional Information
The CacheInvalidationBackgroundManager is a daemon thread will check for updated invalidation records at a frequency defined by the Preference identified with the property CacheInvalidationControlPeriod. If the preference does not exist, the records will be checked every 10 seconds by default.
The CacheInvalidationBackgroundManager implements a JMX MBean interface. This makes it possible to use a JMX client to start and stop the daemon as well as to force the invalidation of an individual cache by invoking the invalidateCache operation with the appropriate Search Key as argument.