A caching pattern for models
by Pascal Opitz on April 6 2009, 11:50
This is a caching pattern for models using Zend_Cache and the __call magic method.
The Basic Idea
The initial thought behind this is that a model should be able to return calls either uncached or cached, without initializing some cache object every time. It should be easy to switch between the two calls, and the cache should be coming with the model object already.
The old way
Before introducing the cache pattern, I would create an object that then would use Zend_Cache_Frontend_Class, then initialize a new instance of my model in there, then redefine a method that acts as somewhat like a proxy, but uses the cache. And if that wasn't enough, I'd have to initialize them both, like in this example:
$model = MyModel();
$model_cached = MyModelCached();
$values_direct = $model->doStuff();
$values_cached = $model_cached->doStuff();
The new way
So Matthias asked me: Would it not be nicer if the cache was already available in the model, and you could do something like this:
$model = MyModel();
$values_direct = $model->doStuff();
$values_cached = $model->cache->doStuff();
A great idea. As you can see this proposed way saves a bit of code, it's intuitive and easy to change, if you ever need to switch between cached and non-cached calls.
The Code
First we need to create an object that initializes the cache. We'll initialize a cache with Zend_Cache and then utilize the __call function to check whether a call exists in the cached object or not. This enables us to only do valid calls.
class BaseModelCache {
private $object;
private $cache;
public function __construct($object) {
$backendName = 'File';
$frontendName = 'Class';
$frontendOptions = array(
'lifetime' => 1800,
);
$backendOptions = array(
'cache_dir' => '/my/cache/dir/',
);
$this->object = $object;
$frontendOptions['cached_entity'] = $object;
try {
Zend_Loader::loadClass('Zend_Cache');
$this->cache = Zend_Cache::factory($frontendName, $backendName, $frontendOptions, $backendOptions);
} catch(Exception $e) {
throw($e);
}
}
public function __call($method, $args) {
$class = get_class($this->object);
$class_methods = get_class_methods($class);
if(in_array($method , $class_methods)) {
$caller = Array($this->cache, $method);
return call_user_func_array($caller, $args);
}
throw new Exception( " Method " . $method . " does not exist in this class " . get_class($class ) . "." );
}
}
The second step is to create a base model class, that we'll use for all our models. It will initialize the cache object using the BaseModelCache and make it available as public property:
abstract class BaseModel {
public $cache;
public function __construct() {
$this->cache = new BaseModelCache($this);
}
}
Et VoilĂ ! Now we can create models by extending the BaseModel. The cache will be available as described above.
Comments
Hi Ludwig. When we developed this our models where based on web services. Also, I never really used Zend_Db_Table_Row, but instead create an instance of Zend_DB in my model and use it from within the methods. Always disliked the active record approach to be honest.
Is the ID problem similar to what Sascha-Oliver described? Maybe when I have spare time, I will have a ponder and see what I can come up with ...
by Pascal Opitz on May 17 2009, 08:43 #
Hi,
Just played around with this the last couple hours.
Its nice but not perfekt. eg. you cant use findParentRow or getDependentRowset when the Zend_Db_Table_Row is cached.
You can get around this by using a own Zend_Db_Table_Row class which overrides setTable. (you can not just set "setTable" couse you loose the _tableClass property, and this is checked in setTable.
If you got around this you fall into your next problem. The ID which is generated by "Zend_Cache_Frontend_Class::_makeId" dont take the contents of the object in account. so if you loop through a rowset and use findDependentRowset in this loop, it will always return the same contents, because the ID generated for it is always the same.
For replication here are the classes I used:
by LudwigR on May 17 2009, 00:36 #
I think the 'extends' solution isn't very flexible. In the real world the model objects usually extend a base model object, which requires the BaseModelCache to be embedded in it or something similar.
Adding public function 'cache' to an existing model wolud be much simpler than extending.
by Stoyan on May 12 2009, 09:25 #
Great. I wonder whether the cache suffix could be dynamically created from the passed params. I'll have a look into it.
by Pascal Opitz on April 23 2009, 11:30 #
Hi Pascal,
you can create the suffix dynamically, but i would prefer not to do so. The Problem is that sometimes you give an object to the constructor and then you have to serialize the whole object to create a cache suffix, and that is too much, i guess. It seems to be better, when you create the suffix manually.
By the way, I made a mistake in the code above. the BaseModel.php won't need the setCacheSuffix, clean, setTagsArray, setPriority & setLifetime methods, because the cache property is public, so you can call it like this:
by Sascha-Oliver Prolic on April 23 2009, 12:22 #
What about serialize, then md5 it?
by Pascal Opitz on April 23 2009, 12:25 #
Hi Pascal,
this will work, but serializing big and heavy objects like Zend_Date f.e. will take a lot of time.
But, if you like, you can use this:
by Sascha-Oliver Prolic on April 23 2009, 12:32 #
Hi,
With the version of Sascha-Oliver i was able to get around this. but its not a very beautiful solution.
The nice thing about it, it doesnt matter whats in the cache and what is fetched from the db. all combinations are working.
You can find the classes at http://svn.cwd.at/cwd/trunk/Cwd/
by LudwigR on May 17 2009, 09:41 #
Hi again,
forgot to add the (quick & dirty) test case: The setTable call is neccessary if the row object cames from cache. (trying to figure out a way to do this automaticly)
by LudwigR on May 17 2009, 00:43 #
Hi Pascal,
here is my proposal, I got already some properties from my config object. this will work, but i didn't wrote any unit tests, yet. please feel free to contact me:
by Sascha-Oliver Prolic on April 23 2009, 11:20 #
I didn't try objects with different constructor parameters, but i *assumed* that passing the object itself as cached entity would take care of this. Will need to have a think, and read the code for Zend_Cache_Frontend_Class.
by Pascal Opitz on April 23 2009, 10:19 #
I see the following problem:
I can work around, when setting cachedEntityLabel by myself, but this property of the Zend_Cache_Frontend_Class is set to private. So how would you cache objects with different constructor params?
by Sascha-Oliver Prolic on April 23 2009, 10:07 #
Hi Pascal,
here is a little optimization of your code:
So you do not have to call the same methods again and again, because the information about the existing class methods never change within the runtime. But if so (maybe you have a dynamic linking plugin object, you can return the get_class_methods() result instead of setting the internal $__objectMethods - maybe per param flag for the method.
A really nice solution, but not applicable in my projects.
by Robert Kummer on April 17 2009, 06:08 #
by Pascal Opitz on April 17 2009, 07:53 #
by Rick on April 17 2009, 10:05 #
One solution for invalidating the cache is deleting it on model updates and writes. You could just incorporate a clear function, so that from within the model you could do $this->cache->clear().
If you're looking to create a more granular caching/uncaching, that gives you control over what you delete record by record, you might want to have a look into the use of tags. Zend_Cache_Class has a function called setTagsArray(), and the clear() also takes tags as an optional parameter.
by Pascal Opitz on April 19 2009, 06:49 #
by David on April 18 2009, 21:47 #
The main idea behind the concept is that the site this is used in is split up into lots of different, independent applications, and against my MVC preferences the main logic takes place in the controllers.
This meant 2 things for me:
If you were to build an application from scratch, and you knew exactly which data sources to cache, you might want to integrate the caching into your model, but in my case I'm iterating over the code base over and over again, refactoring and improving in small steps on a constant basis, and this is where I feel the approach was perfect.
by Matthias Willerich on April 8 2009, 10:27 #
by Alex on April 6 2009, 16:18 #