TypeConverter

A class that handles the detection and conversion of certain resource formats / content types into other formats.

Caching each query individually

Thursday, April 16th 2009, 2:34am
Topics: Tutorials, CakePHP
Tags: Model, Cache, Query, SQL, Find
Comments: 8
Permalink - Tinylink

So lately I have been delving into the caching capabilities of CakePHP. Most, if not all of its capabilities work wonderfully; although I personally can't get into $cacheAction (within the controllers). The $cacheAction property only works for static and non-user generated pages, in other terms, any content that changes depending on a logged in user wont work correctly with $cacheAction (unless you want thousands and thousands of cache files). So I stopped using $cacheAction all together in my latest application, and instead built a method that caches individual queries, instead of the whole page. All the modifications have been applied to the models find() method. To use this, place the following code within your app/app_model.php.

/**
 * Wrapper find to cache sql queries
 * @param array $conditions
 * @param array $fields
 * @param string $order
 * @param string $recursive
 * @return array
 */
function find($conditions = null, $fields = array(), $order = null, $recursive = null) {
	if (Configure::read('Cache.disable') === false && Configure::read('Cache.check') === true && isset($fields['cache']) && $fields['cache'] !== false) {
		$key = $fields['cache'];
		$expires = '+1 hour';
		
		if (is_array($fields['cache'])) {
			$key = $fields['cache'][0];
			
			if (isset($fields['cache'][1])) {
				$expires = $fields['cache'][1];
			}
		}

		// Set cache settings
		Cache::config('sql_cache', array(
			'prefix' 	=> strtolower($this->name) .'-',
			'duration'	=> $expires
		));
		
		// Load from cache
		$results = Cache::read($key, 'sql_cache');
		
		if (!is_array($results)) {
			$results = parent::find($conditions, $fields, $order, $recursive);
			Cache::write($key, $results, 'sql_cache');
		}
		
		return $results;
	}
	
	// Not cacheing
	return parent::find($conditions, $fields, $order, $recursive);
}


In the next step, you would create a folder called sql within your tmp/cache/ and chmod the permissions to 777. Once you have created the folder, open up your app/config/core.php file and place the following code at the bottom (near the default cache settings).

Cache::config('sql_cache', array(
    'engine'		=> 'File',
    'path'		=> CACHE .'sql'. DS,
    'serialize'	=> true,
));


By default, caching will not work on your applications queries, you would need to set an additional "cache" option within your find(). Each SQL cache should have its own unique identifier so that it does not conflict with other queries. Also by default, queries will be cached for one hour and will be saved as a serialized array. The following examples explain how the cache option works.

// Cache query to /tmp/cache/sql/model-test_sql_query
$results = $this->Model->find('all', array(
	'cache' => 'test_sql_query'
));

// Cache query to /tmp/cache/sql/model-another_query that expires in 24 hours
$results = $this->Model->find('all', array(
	'cache' => array('another_query', '+24 hours')
));


What if I have a query that's used multiple times but each has its own limit (custom method), but uses the same cache slug? Simply give the cache slug a dynamic name like so:

// Cache query to /tmp/cache/sql/model-dynamic_query-15 
$results = $this->Model->find('all', array(
	'limit' => $limit, // 20, 30, etc
	'cache' => 'dynamic_query-'. $limit
));


I personally have found an increase in load times up to 150-200% faster using this method. This should only be applied to queries that are used on landing pages, and queries that do not change according to which user is logged in. Have fun.
Related Entries:

8 Comments

10 / 2 = ?
Allowed: [code] [b] [i] [u]
  • Brad V.
    bleakworld.com
    Apr 16th 2009, 10:10
    1 Would there be a way to auto-md5 hash the SQL query instead of giving each cached query a name? That would make each query even with a limit or dynamic element be individually cached and the only setting how long you would want the query to be cached for.

    Not sure how to create that callback to get the final sql query back into AppModel to md5 it though.

    Brad
  • Miles Johnson
    milesj.me
    Apr 16th 2009, 15:03
    2 @Brad - Im sure you could just wrap and md5() around the key, but im not sure how we could do it without defining the key.

    Cache::read(md5($key), 'sql_cache');
    Cache::write(md5($key), $results, 'sql_cache');

    I tried getting the parent method name and using that (Model::customMethod()), but a lot of the time there isn't one.
  • primeminister
    cake-toppings.com
    Apr 27th 2009, 03:11
    3 Why not put the model, conditions and fields into an md5() and use that key. Should be sufficient I guess.
    Then you don't have to put the cache key yourself and if you write queries twice in different controllers with some associations it is used again.
    Or am I missing something...
  • mark
    Jun 8th 2009, 06:07
    4 I guess somebody already had this idea quite some time ago
    md5 of all the input vars:
    http://blog.awpny.com/2007/10/caching-model-data-in-cakephp-part-2/
  • VoiDeT
    jotlab.com
    Feb 7th 2010, 19:24
    5 I forgot to add in an array_merge there :)
  • Kevin
    mainelydesign.com
    Mar 24th 2010, 08:28
    6 Miles- awesome approach! I'm already using this in my application. I made one change- if (Configure::read('Cache.disable') === false... to if (!Configure::read('Cache.disable') ... because core.php ships with that line commented out, not set to false
  • thomas
    Apr 1st 2010, 06:19
    7 thank you miles. :-)
  • Mark Henderson
    lex.ptc.com
    Feb 21st 2011, 06:50
    8 Here's a question for you - how possible would it be to use the same method for caching calls to "paginate?"