Entity Caching Strategies with Drupal

There comes a time where you sit back and look at your Drupal site and realize that you've created a monster. You try to refactor your code, but there is only so much you can do, as there is genuine level of complexity behind how all of your different node types render out their fields. Or, perhaps your site isn't complicated, but has been ground to a halt by the sheer number of entities that must be loaded to render a single page.

Doubtless, you've heard of caching once or twice, but your caching strategy gets complicated when you're rendering nodes on dynamic pages, such as a View with exposed filters or search results where the entire output of a page is almost always unique. However, the individual entities stay the same, or at least until they are changed, and whenever something stays the same the opportunity for caching is introduced. Let me show you how it’s done.

Load Caching

One of the first steps in displaying any content is retrieving the information from the database. Normally this isn't a problem, but as your entities increase in complexity with additional fields, the queries required to return a fully structured entity can turn into a bottleneck on your site. This is where Entity cache comes into play. It utilizes the new Drupal 7 entity API to cache entire entities, thus removing the need to perform time consuming queries. Win!

There is one catch to this approach, it requires that you always update your entity tables using the save/delete API calls in order to ensure Entity cache knows when to invalidate the cache. But you are doing that already, right?

This technique can be further enhanced by using the the Drush Entity Cache Loader module, which provides a command line interface that allows you to prime your entity cache all at once or by entity type.

Render Caching

With our entities safely cached and ready to go, we now turn our attention to the theme layer where we have to create a render array and run through our often time consuming preprocess logic for every entity. This is where Display Cache cache comes in to help us out. Display Cache only reports a few site installs at the moment, but it appears to be a solid module that anyone who needs this functionality should look into using and supporting. It offers controls on a per node type basis, in addition to allowing controls for individual fields within each node type. We haven't used Display Cache beyond testing at the moment, but we will definitely be looking into it the next time a need like this arises.

It should be noted that Display Cache is only available for Drupal 7, and many large websites will probably continue to use D6 for another couple years. To bridge the gap, the Advanced cache module offers somewhat similar functionality for D6, but it only has a dev release and doesn't have any recent activity. Additionally, the custom solution outlined below could be extended to offer Display cache like functionality for Drupal 6.

Views Caching

We are getting to a good place now, our entities are cached along with their rendered markup for various view modes. However, Views content cache allows us to take caching one step further. While it is only in alpha at the time of this writing, in testing it offered powerful and easy to use controls that allow for cache control on a per view basis. It offers cache invalidation by node type and max/min cache times for the query set results as well as the full rendered markup of the view, which gives you another layer of caching with great invalidation controls. There is one caveat to this module. As noted in the documentation, it doesn't support views with exposed filters due to an outstanding issue with views.

Custom Solutions

With the incredible solutions above, is there even a need to implement a custom caching solution for any of these layers? The answer is probably "no" for most, but we needed a way to cache the rendered markup from some lengthy preprocess functions for search results in Drupal 6. The core functionality from our implementation is included below for any of you who think you may need a custom solution.

Alter the theme registry

First we need to alter the theme registry a bit. We do this by cloning our existing theme entry (for later use) and overriding it with a new entry that uses a function as the callback instead of a template file. This is probably the most important step, as it completely hides the caching functionality from the rest of Drupal. This allows you to still use the theme layer without Drupal ever knowing about the magic happening in the background.

/**
* Implements hook_theme_registry_alter().
*/

function example_theme_registry_alter(&$theme_registry){
// Duplicate the standard node theme entry.
$theme_registry['node_copy'] = $theme_registry['node'];
// Overwrite the existing node theme entry.
$theme_registry['node'] = array(
'function' => 'example_render_template',
'theme path' => path_to_theme(),
'type' => 'module',
'render element' => 'elements',
);
}

Set up a theme callback function

This function now returns the fully rendered markup for a node, since we are no longer rendering the node through a template.

/**
* Callback theming function for nodes.
*
* @param $variables
* An array on theme variables.
*
* @return
* The fully rendered node markup.
*/

function example_render_template($variables) {
$markup = '';
// Get the node object.
$node = $variables['elements']['#node'];
// Get the cache id.
$cache_id = example_cache_id($node);
// The cache exists and is valid.
if (($cache = cache_get($cache_id, 'cache_node')) && !empty($cache->data)) {
$markup = $cache->data;
}
// Render out a new node.
else {
// Render the node.
$markup = theme('node_copy', $variables);
// Delete the old cached values.
$cache_id_delete = sprintf('node_%d', $node->nid);
cache_clear_all($cache_id_delete, 'cache_node', TRUE);
// Cache the rendered markup.
cache_set($cache_id, $markup, 'cache_node');
}
return $markup;
}

Create cache id values

The trick that makes this all work is the inclusion of the node's vid in the cache id. This ensures that a stale cache value will never be accessed, eliminating the need to trigger a cache invalidation each time a node is updated.

/**
* Creates a cache id for a given node.
* Format: node_[nid]_[vid].
*
* @param $node
* A standard Drupal node object.
*
* @return
* The current cache id for the node or FALSE if unsuccessful.
*/

function example_cache_id($node) {
// Ensure that a nid is available.
if (!isset($node->nid)) {
watchdog('Error getting the cache id because no nid could be found.');
return FALSE;
}
// Create and return the cache id.
$cache_id = sprintf('node_%d_%d', $node->nid, $node->vid);
return $cache_id;
}

Add cache clearing and drush integration

Since we created a new cache table, we now need to register a new cache clearing function to allow the cache to be cleared from within the Drupal UI. While we are at it, we add drush hooks so the new cache table can be cleared via drush as well.

/**
* Implements hook_drush_cache_clear().
*/

function example_drush_cache_clear(&$types) {
$types['node'] = 'example_cache_clear_all';
}

/**
* Callback function for @see hook_drush_cache_clear.
*/

function example_cache_clear_all() {
cache_clear_all('*', 'cache_node', TRUE);
}

/**
* Implements hook_flush_caches().
*/

function example_flush_caches() {
return array('cache_node');
}

So whether you implement something custom or utilize one or all of the modules outlined above, we hope we have opened your eyes to a new layer of caching that you can add to your site to make it even faster. However, we would be remiss if we did not mention that this isn't a full caching solution. Even with all of this in place, there is still plenty of room for memcache, APC, varnish, and all of the other caching tools that are available. So cache early and cache often, just make sure you invalidate your cache correctly along the way.

Roadmap Your Drupal 7 Transition

We’re offering free 45 minute working sessions to help you assess your organizations level of risk, roadmap your transition plan, and identify viable options!

Drop us a note, and we’ll reach out to schedule a time.