ColdFusion Quickie: Simple way to cache by arguments passed to a method

This post is more than 2 years old.

Just a quick tip for folks using ColdFusion 9's new caching system. Whenever you add something to the cache you must use a unique key. For a simple method, that's relatively easy:

public any function doItSlow() { var cachedData = cacheGet("slowProcess"); if(isNull(cachedData)) { var cachedData = doSomethingSlow(); cachePut("slowProcess", cachedData); }

return cachedData; }

That's a pretty trivial example, but you get the idea. A problem may arise, though, when your method takes arguments, especially multiple arguments. Imagine a typical "getX" type function that loads data. It may take arguments for the max number of items to return, the starting index, the order, and perhaps a search string. Imagine the following method signature:

function getStuff(numeric maxItems=10, numeric start=1, string sort="name asc", string search) {

Given that you have multiple arguments and therefore a large number of unique results, how could you cache the results? You could generate a key by examining the arguments scope:

var key = ""; key &= "#arguments.maxItems#_"; key &= "#arguments.start#_"; key &= "#arguments.sort#_"; if(structKeyExists(arguments.search)) key &="#arguments.search#";

This would create a key looking something like: 10_1_name_asc_foo. While this method works, it can get pretty complex. (Although if you start adding more arguments, you may have an overly complex API in the first place. That's a discussion for another day.) Here is a little trick I've taken to using that makes it even easier:

var key = "mymethod_#serializeJSON(arguments)#";

That's it. I use a prefix of "mymethod" so that when I examine the entire cache it is clear as to where the data belongs. The rest of the key is simply a serialized form of the arguments. It may be ugly, but it works perfectly well. Obviously you want to use with caution - especially since that search argument could be anything.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Brad Wood posted on 1/23/2010 at 10:51 PM

It's worth noting caching a method's output as a function of its parameters will only work for deterministic methods. A non-deterministic method is not based solely on the arguments, but external sources. For instance, a method that accepted a date and returned a time span based on a dateDiff calculation using now().

I do like your idea of using serializeJSON. I generally have just concatenated and hashed the arguments (assuming they are simple values) but serializeJSON is a bit less code and I could still hash it just to cut down on the sheer size of the cache key.

Comment 2 by Raymond Camden posted on 1/23/2010 at 10:54 PM

Good points. Yeah, the hash would make it cleaner. Although in JSON form, I can still "read" it. I wouldn't expect a non-tech person to be able to read it, but then again, a non tech person wouldn't be examining the cache probably. Of couse, in JSON form, you could do some more deep reporting on the cache items, since you can deserialize it to examine how the method was used.

You know - ignore cache for a minute. It could be kinda useful to just log the serialized args in general. You would want to use ColdSpring though and an... aspect (may be wrong on the term there) so you aren't littering your model.

Comment 3 by Henry Ho posted on 1/23/2010 at 11:40 PM

hmm.. would SerializeJSON() guarantee the order of arguments though? Argument scope is weird 'cause it behaves like an array and a struct at the same time. What happen if someone pass in an ArgumentCollection? Will there be a chance that the JSON string have the same arguments, but in different order?

Comment 4 by Raymond Camden posted on 1/23/2010 at 11:43 PM

The order will be consistent if the caller is consistent. In a typical MVC app that means everything goes to the service via the controller.

If not, then your results may vary. ;)

Comment 5 by Tyler Clendenin posted on 1/24/2010 at 2:37 AM

Can a hash method be used on a Coldfusion structure?

Comment 6 by Brad Wood posted on 1/24/2010 at 2:46 AM

http://www.cfquickdocs.com/...
Input is a string.

That is why you would need to turn the struct into a string with a function such as serializeJSON.

Watch out for complex arguments like an object with references to your framework. That could get interesting.

Comment 7 by Ben Nadel posted on 1/24/2010 at 6:20 AM

As someone who is a super fan of hash()ing values to get KEYs, I think this approach is really cool. Way to think outside the box.

Comment 8 by Raymond Camden posted on 1/24/2010 at 6:21 AM

To be honest, I've been on a huge JSON kick lately. I also love it for logging too.

Comment 9 by Brad Wood posted on 1/24/2010 at 7:19 AM

Well in the past our only real option for representing complex variables as a string was wddx. JSON just seems to much more lightweight and I think it's easier to eyeball values out of too.

Comment 10 by Mike Kelp posted on 1/26/2010 at 2:25 AM

Have you considered Java's hashCode() method?

See JavaDocs for java.util.HashMap as my comment was flagged as spam with the link in it.

You could potentially use this to store an identifier for the different combinations of arguments in the whole arguments structure, or make a small structure of just the arguments you care about. Under the hood, CF structures / scopes implement java.util.HashMap and I've used this before to compare de-dupe structures of data pulled from xml services for instance. It's also intended for this type of comparison, with the hash function being designed for wide variance in even slightly different input.

Ultimately though, a hash by definition can have collisions so I would only suggest doing this in cases where you expect relatively few combinations like a finite argument set. If the cache arguments can vary to a massive degree because of id's, search strings, etc. it isn't a great idea to cache them all anyway.

Comment 11 by Tyler Clendenin posted on 1/26/2010 at 4:01 AM

Though it is true that a hash can have collisions, the likelihood of that happening is very very very slim. Almost as slim as a GUID not being unique within the next 50 years.

Comment 12 by Brad Wood posted on 1/26/2010 at 4:19 AM

Did you know every time you reply on Ray's blog his comment table burns a perfectly good GUID that will never get to be re-used. For goodness sake people, GUIDs aren't a renewable resource, we need to conserve them!
:)

Comment 13 by Raymond Camden posted on 1/26/2010 at 4:29 AM

Hahahahah - nice. :)

Comment 14 by Ryan Stille posted on 8/11/2012 at 12:38 AM

Interesting idea, I was thinking along the same lines... but then realized you can't use cache keys with commas in them since the caching functions take a comma separated list.

You could use something like
cacheKey = Replace(SerializeJSON(arguments),",","","all")

Comment 15 by Raymond Camden posted on 8/11/2012 at 12:47 AM

Sorry - what? cachePut/Get take an ID value that is just an id. So "a,b" as an ID should be legal.

Comment 16 by Ryan Stille posted on 8/11/2012 at 1:01 AM

Serialized JSON (as suggested above) will contain commas. You can't use commas in your cache keys. Take this code for example:

<cfset cacheKey = "test,or serialized JSON here">
<cfset cachePut(cacheKey,"foo") />
<cfset cacheRemove(cacheKey) />
#cacheGet(cacheKey)#

The cacheGet call will return "foo", even though I've tried to delete it on the line above.

Comment 17 by Raymond Camden posted on 8/11/2012 at 1:03 AM

Wow. I'd call that a bug then. Or at least it should throw an error. If I use key a,b and it stores it as A w/o warning, that's bad. Making a test case and will write a bug report.

Comment 18 by Raymond Camden posted on 8/11/2012 at 1:04 AM

I'm not able to reproduce this using this code:

<cfset cachePut("foo,moo", 1)>
<cfdump var="#cacheGetAllIds()#">
<cfset v = cacheGet("foo,moo")>

<cfoutput>#v#</cfoutput>

I'll try with yours.

Comment 19 by Raymond Camden posted on 8/11/2012 at 1:05 AM

Ok, to me, the bug is in cacheRemove. It fails to remove the key. So you can use commas for put/get, you just can't delete it. Can you confirm that?

Comment 20 by Ryan Stille posted on 8/11/2012 at 1:08 AM

Yes thats exactly what I was seeing. It gets placed into the cache, but it can't be removed. My guess is this is because cacheRemove() accepts a comma separated list of IDs to remove.

Comment 21 by Raymond Camden posted on 8/11/2012 at 1:08 AM

Even odder - if you use

cacheRemove(cachekey, true)

it throws an error, even though true is supposed to be the default.

Comment 22 by Raymond Camden posted on 8/11/2012 at 1:12 AM

Filing two bug reports.

One - inability to remove a key with commas when you are allowed to set it.

Two - if I do cacheRemove(.., true) it acts differently, which doesn't mesh with the docs that say exact=true is the default.