Easily Cache the Return Value of Any Function
Work in web development long enough, and you'll run into caching. When done appropriately, it can be a real life saver. In CFML, we have several caching tools at our disposal.
The built in cfcache function will cache an entire page, and there are a bunch of projects on RIA Forge (search for "cache") that deal with caching of whole pages, partial pages, and specific data; and use various methods from memcached to underlying java caching. And of course, you can always roll your own cache using a persistent scope (application, session, client, or cookie) and some date comparison.
After about the 100th time of rolling my own custom caching, I decided it was time to wrap it up in a nice little UDF.
Before I show you the code, here's an example of how to use it:
<cffunction name="currentTime" output="false">
<cfreturn "The current time is #timeformat(now())#<br />"/>
</cffunction>
#cacheCallback("myApp.CurrentTime", CreateTimeSpan(0,0,2,0), variables.currentTime)#
Pretty simple, right? The first argument is a unique string that's used as the cache key, to identify which bit of data we're caching. The second is how long the cache is good for, and the third argument is a reference to a simple function that displays the current time.
Notice that the 3rd argument is not a string containing the name of the function to be executed, but actually a reference to the function itself. My cacheCallback function will check to see if the cache is expired, and either return the cached value or re-run the function being passed in and return the result, as appropriate.
A fourth and optional (boolean) parameter will force a cache refresh if true.
This has already proven useful, as a fellow Mango Blog plugin developer is using it to cache API results from services like Twitter and Flickr in some plugins he'll be releasing soon.
Here's the code for the cacheCallback function:
<cfscript>
function cacheCallback(cacheKey, duration, callback){
var data = "";
//optional argument: forceRefresh
if (arrayLen(arguments) eq 4){
arguments.forceRefresh=arguments[4];
}else{
arguments.forceRefresh=false;
}
//clean cachekey of periods that will cause errors
arguments.cacheKey = replace(arguments.cacheKey, '.', '_', 'ALL');
//ensure cache structure is setup
if (not structKeyExists(application, "CCBCache")){
application.CCBCache = StructNew();
}
if (not structKeyExists(application.CCBCache, arguments.cacheKey)){
application.CCBCache[arguments.cacheKey] = StructNew();
}
if (not structKeyExists(application.CCBCache[arguments.cacheKey], "timeout")){
application.CCBCache[arguments.cacheKey].timeout = dateAdd('yyyy',-10,now());
}
if (not structKeyExists(application.CCBCache[arguments.cacheKey], "data")){
application.CCBCache[arguments.cacheKey].data = '';
}
//update cache if expired
if (arguments.forceRefresh
or
dateCompare(now(), application.CCBCache[arguments.cacheKey].timeout) eq 1){
data = arguments.callback();
application.CCBCache[arguments.cacheKey].data = data;
application.CCBCache[arguments.cacheKey].timeout = arguments.duration;
}
return application.CCBCache[arguments.cacheKey].data;
}
</cfscript>
I've submitted this to CFLib, so hopefully it will be approved soon.
Disadvantages:
I tried for a while to figure out a way to pass arguments into the callback function, but never came up with anything. When I added a callbackArgs argument and passed that in as an argumentCollection to the callback function, CF threw an error telling me that I couldn't use named arguments in this case. I also tried to use CFInvoke, but that required passing in a string containing the name of the function instead of a reference to it, which while it would actually work with some massaging, wouldn't work as well in an Object Oriented architecture where passing objects (like function references) as arguments is common practice.
So for now, your callback functions must run without any arguments. If anyone has any other ideas for how to pass in arguments, I'm all ears.
Posted in ColdFusion | 1 Response October 14 2008