« Microformats CFC mentioned in Open Source Update and ColdFusion Weekly Podcast | Main | Calling CF from a command line? »

Updating cached queries after content changes

One of the finer arts of being a ColdFusion developer is learning how to efficiently cache your queries. CF gives you such an easy and powerful way to cache your queries that it's easy just to start caching particular queries for a specific amount of time and leave it at that. But what if you cache your query for an hour or a day and an editor updates the content in the meantime? The content as it's displayed on the website won't change until the cache expires, which may not please the editor. So you might decrease the duration of your cache's lifetime, perhaps to just an hour. But then you could be refreshing the cache a lot more often than you need to since the editor doesn't make changes more than once a day. So is there a way to get fine-tune control over your query so that it's cached for as long as you need but gets refreshed right after someone updates the content?

Yes there is! Read on and I'll share my technique.

What I tend to do is to cache all entities in the database that the application is going to call frequently, especially if they're often displayed as a group (think of a list of user types, a list of navigation items, or a list of product categories, for example). Let's start with the following query:

<!--- Get a list of all categories, and cache it for one day --->
<cfquery name="qCategory" datasource="#REQUEST.contentDSN#" cachedwithin="#CreateTimespan(1, 0, 0, 0)#">
	SELECT categories.categoryID, categories.categoryName
	FROM categories
	ORDER BY categories.categoryName ASC
</cfquery>

Note that queries are uniquely cached by name and by the SQL code they contain, including whitespace (whitespace means spaces, linebreaks, and tabs). If the SQL code changes, even by whitespace only, the query is sent off to the database and then recached. So to make sure that the whitespace in the query doesn't change, I don't embed the query in different pages-- I put it in a custom tag (let's call it qCategories.cfm) and call it as needed. (Why I use a custom tag instead of an include will become clear in a little while.)

<!--- Initialize parameters --->
<cfparam name="ATTRIBUTES.refresh" default="0">

<!--- Set the cache time to a week or to nothing as needed --->
<CFSET VARIABLES.cachedWithin = IIF(Val(ATTRIBUTES.refresh), 'CreateTimespan(0,0,0,0)', 'CreateTimespan(7,0,0,0)')<

<!--- Get a list of all categories, cached or refreshed as needed --->
<cfquery name="qCategory" datasource="#REQUEST.contentDSN#" cachedwithin="#VARIABLES.cachedWithin#">
	SELECT categories.categoryID, categories.categoryName
	FROM categories
	ORDER BY categories.categoryName ASC
</cfquery>

Note that the custom tag has one attribute called "refresh". If refresh is set to the default value of zero, then the query is cached for a week. If you pass a positive refresh value to the tag, the query is called with a cache duration of 0, which means that it will be sent to the database and recached. So, we now have a means of controlling whether our query is cached. How do we use it?

When you're calling the query to be displayed, most likely on the front end of your site, you can simply call the custom tag, and then refer to the query:

<cf_qCategories>

<cfoutput query="qCategories">#categoryName#
</cfoutput>

But when you're in the backend of the site, where the editors modify the content of the site, you pass the refresh attribute to the custom tag whenever they make a change to the categories:

<!--- Create/update/delete a category --->
<cfquery datasource="#REQUEST.contentDSN#">
     [Whatever SQL code you need to create, update, or delete a category]
</cfquery>

<cf_qCategories refresh="1">

And there's the solution. Using this technique of embedding your queries in a custom tag to give control over caching means that whenever your editors make updates to the site, they'll be gratefied to see their changes show up immediately. But you'll be able keep the benefits of long term query caching, decreasing the load on your database and making your application faster and more responsive. (Note that to use this technique, the you must be using the same application for both your front and back ends.)

Comments (3)

A better solution without involved much of cf logic is to use a combination of CHECKSUM_AGG and Binary_checksum like this

SELECT CHECKSUM_AGG(BINARY_CHECKSUM(*))
FROM categories

. Store this value in a category header table. Then when you want to know if the table data has been changed, you run the above statement again and compare the returned value to the old value you have stored in the header table. If the two values are not the same, then the table data has changed. Re-run your cached query at this time.

@Nick: I disagree that a query to run a checksum is the most efficient way to update cached queries. After all, what we're trying to do is to avoid running a query on each page request, which is what you seem to be suggesting. It's much simpler just to recache the query through application logic when you know for sure that the content has just been updated.

This is exactly what I've been looking for. Thanks Tom.

Post a comment


Type the characters you see in the picture above.