To circumvent the limitations of ColdFusion's built-in query caching method, I developed a custom tag solution that lets you cache queries to memory or to disk. The slight drawback of caching to disk, however, is that caching to disk is often comparatively slower than caching to memory, due to server interfaces and bus speeds.
The inspiration for my cf_extremeQueryCache tag came from
another great custom tag designed for content caching: cf_superCache.
You can download the cf_extremeQueryCache tag from page
one of this tutorial. If you have not yet added cf_superCache
to your development arsenal, I recommend doing so. You can find the
cf_superCache tag by searching the keyword in the Macromedia
ColdFusion Exchange. Additionally, Brandon Purcell is currently
developing a content caching tag called cf_Accelerate,
also inspired by cf_superCache which you can download
from his website.
You can use cf_extremeQueryCache around any ColdFusion
query in order to cache the query either to memory or to disk. The resulting
query cache contains the original query name, the date/time that the
cached query will expire, and the returned recordset.
If you're looking for specific details on how the cf_extremeQueryCache
tag functions or how to use it, open the tag in Macromedia ColdFusion
Studio or in Dreamweaver Code view (or your ColdFusion editor of choice)
and spend some time reading the opening comments, all 175 lines of them.
The opening comments document the specific functionality of the custom
tag, but I will give you a quick overview below.
Use the following simple steps to install and use the custom tag.
Copy cf_extremeQueryCache.cfm into your ColdFusion custom
tag directory at cfusionmx/customtags (or cfusion/customtags directory,
if you're using an older version of ColdFusion). Once you copy the extremeQueryCache.cfm
file into the customtags directory, all of your ColdFusion web applications
can access the cf_extremeQueryCache custom tag. This is
a one-time step; you do not need to repeat this step every time you
would like to use the custom tag. If you're a JRun user, read all about
Creating
and Using Custom Tags in the ColdFusion LiveDocs.
This step really is subjective, since each developer will have his own opinion on what makes a query a good caching candidate. However, your criteria for selecting a query to cache will be different than it has been in the past since this custom tag can cache any number of queries in memory or onto disk.
Since the ColdFusion Administrator setting, Maximum number cached queries, limits ColdFusion's built-in query caching methods, the criteria for selecting a query to cache previously often met the following conditions:
For example, the following query retrieves a list of state names and abbreviations; this is a good candidate for caching through ColdFusion's built-in query caching method.
<cfquery name="qStates" datasource="myDSN"> select strAbbreviation, strName from tblStates order by strName </cfquery>
Conversely, since available server RAM and disk space are the only
limitations for the cf_extremeQueryCache custom tag, the
criteria for selecting queries to cache becomes much more flexible:
For instance, the following query returns all titles that a user has access to and which begin with a letter that the user selected:
<cfquery name="qUserTitles" datasource="myDSN"> select t.uuidTitle, t.strTitle from tblTitle t, tblXREF_Title_User xtu, tblUser u where u.user_id = #request.user_id# and u.user_id = xtu.user_id and xtu.title_id = t.title_id and t.strTitle like '#form.strFirstLetter#%' </cfquery>
Note: Applies to disk caching method only. Skip to Step 4 if you want to cache to memory.
If you choose to have the cf_extremeQueryCache custom
tag write cached queries to disk, you must create one or more directories
to store the cached queries. I strongly recommend reviewing the Caution
When Caching to Disk section of this article before choosing to
cache queries to disk.
When using the cf_extremeQueryCache custom tag to cache
your queries to disk, I recommend the following:
As a security precaution, it's important that you put all your cached queries outside of the web document root if you will be caching security-sensitive information. If users find your query caching directories while poking around your website through their web browser, then they could download the WDDX files, snoop through them, and in short, browse through your database records.
One example of recommended directory structure is as follows. As with
the qUserTitles query above, I created two directories:
C:\cfusionmx\extremeQueryCache\C:\cfusionmx\extremeQueryCache\qUserTitlesIn cases where you are running more than one web application under the same instance of ColdFusion, consider creating directories for each of your applications inside of the extremeQueryCache directory, placing query directories beneath their corresponding application. This will help prevent problems that might arise if you begin caching queries with the same name across multiple applications. In this case, your directory structure would be as follows:
C:\cfusionmx\extremeQueryCache\C:\cfusionmx\extremeQueryCache\myAppName\C:\cfusionmx\extremeQueryCache\myAppName\qUserTitlesBy default, the cf_extremeQueryCache custom tag will throw
an error if the cache directory specified in your tag attribute does
not already exist on the file system. Since I included the custom tag
file in this article, you could modify the custom tag's code to dynamically
create cache directories rather than throw an error. When I created
the tag, I wanted explicit control of the location and creation of query
caching directories, so I did not build this functionality into the
tag.
To use the tag, simply wrap it around the CFQUERY that
you would like to cache and specify a few attribute values.
<cf_extremeQueryCache queryName="" cacheName="" cachedWithin="" cacheScope|cacheDirectory="" > <cfquery name="qUserTitles" datasource="myDSN"> select t.uuidTitle, t.strTitle from tblTitle t, tblXREF_Title_User xtu, tblUser u where u.user_id = #request.user_id# and u.user_id = xtu.user_id and xtu.title_id = t.title_id and t.strTitle like '#form.strFirstLetter#%' </cfquery> </cf_extremeQueryCache>
The queryName attribute specifies the name of the query
that you are caching between the tag's open and close brackets. In this
case, the queryName attribute value is qUserTitles.
<cf_extremeQueryCache queryName="qUserTitles" ... > ... </cf_extremeQueryCache>
This is the trickiest tag attribute. The cacheName attribute
specifies a value that uniquely identifies each possible recordset that
the query could return. The simplest way to create a unique value is
to create a concatenated string that combines the input parameter values
in the query. In the example, there are two input parameters (surrounded
by pound signs) that affect the record returned by the query: request.user_id
and form.strFirstLetter. Therefore, the cacheName
attribute value would be:
<cf_extremeQueryCache queryName="qUserTitles" cacheName="qUserTitles_#request.user_id#_#form.strFirstLetter#" ... > ... </cf_extremeQueryCache>
If you're familiar with ColdFusion built-in query caching, then this parameter is no mystery. The value of this parameter specifies how long the server will use the cached query results before running the query again and generating a new cached query.
Determine the cachedWithin value on a case-by-case basis
for each query you cache. Consider how often a query's recordset will
change. A query that returns state names is unlikely to change, so it's
safe to cache the query for a long time. However, a query to an "orders"
table for an e-commerce site will likely change all the time as customers
submit new orders.
In the qUserTitles example, the query returns a list of
titles beginning with a letter that a user selects. Since the titles
catalog doesn't change often, caching the query on a daily basis is
sufficient.
Set the cachedWithin parameter with a time span using
the ColdFusion createTimeSpan() function. Since the function
takes parameters of days, hours, minutes, and seconds, you can set the
value as follows:
<cf_extremeQueryCache queryName="qUserTitles" cacheName="qUserTitles_#request.user_id#_#form.strFirstLetter#" cachedWithin="#createTimeSpan(1,0,0,0)#" ... > ... </cf_extremeQueryCache>
If you are caching the query to memory, use the cacheScope
parameter. If you are caching the query to disk, use the cacheDirectory
parameter. When using the custom tag, pass only one of these parameters.
cacheScope
This attribute specifies which variable scope to store the cached query
in. You can specify a value of server, application,
or session. The cached query will be placed in the specified
scope as part of a structure called stcExtremeQueryCache.
<cf_extremeQueryCache queryName="qUserTitles" cacheName="qUserTitles_#request.user_id#_#form.strFirstLetter#" cachedWithin="#createTimeSpan(1,0,0,0)#" cacheScope="application" > ... </cf_extremeQueryCache>
cacheDirectory
This attribute specifies the full directory path of the cache directory.
<cf_extremeQueryCache queryName="qUserTitles" cacheName="qUserTitles_#request.user_id#_#form.strFirstLetter#" cachedWithin="#createTimeSpan(1,0,0,0)#" cacheDirectory="c:\cfusionmx\extremeQueryCache\qUserTitles\" > ... </cf_extremeQueryCache>
At this point, you could save the changes to your code, point your web browser at the page, and fire away. However, I think it's important to take a moment and measure the performance gain you achieve by using this custom tag query caching method. After all, the proof is in the pudding, right? Because I'm certain that pulling a query from the cache will be much faster than executing the query every time, I've added a built-in method for measuring the performance gain, so you can see the difference for yourself.
By simply adding the debugMode attribute with a value
of true to the cf_extremeQueryCache tag
call, the tag will automatically display a JavaScript alert indicating
the execution time (in milliseconds) and whether the server executed
the query or retrieved it from cache. Of course, remove the debugMode
attribute from your code before releasing your changes to production.
<cf_extremeQueryCache queryName="qUserTitles" cacheName="qUserTitles_#request.user_id#_#form.strFirstLetter#" cachedWithin="#createTimeSpan(1,0,0,0)#" cacheScope="application" debugMode="true" > ... </cf_extremeQueryCache>
If you choose to cache queries to memory, I recommend you keep a close
watch on your server memory usage once you apply this tag. Since this
tag will continue to place more and more cached queries into memory,
there is the risk that an extremely large volume of cached queries could
drain your server's available RAM and cause overall performance issues
for the server. To avoid this, consider running a nightly ColdFusion
scheduled task to clear any cached queries from memory (see opening
comments of the cf_extremeQueryCache custom tag for details
on clearing cached queries).
If you choose to cache queries to disk, consider the following points. While available disk space is the primary limitation of this caching method, there is another limitation not immediately evident. In developing and testing this custom tag, I found another limitation that directly affects the successful implementation of this custom tag when caching queries to disk. While I've not extensively tested it, I believe this issue only affects users running on ColdFusion MX. The limitation has to do with an internal server setting that governs the "entity expansion limit" of ColdFusion. When caching to disk, this custom tag creates WDDX packets storing all the cached query information. If ColdFusion attempts to deserialize a WDDX packet that exceeds the "entity expansion limit," the server throws an error and is unable to deserialize the packet. Because my custom tag uses WDDX packets heavily when caching to disk, with the potential of exceeding the limit, I ran into a problem. Fortunately, I have planned for this particular situation, should you encounter it.
If the cf_extremeQueryCache tag attempts to deserialize
a disk-based WDDX packet that exceeds this limit, the tag will throw
a custom error message explaining the cause of the error and the steps
for increasing the "entity expansion limit," eliminating the problem.
Check out Technote
19125: ColdFusion MX 6.1: Error parsing large XML documents, for
more information.
If you'd like to eliminate the potential limitation before encountering it, read on. The default value of the "entity expansion limit" is 64000. You can modify this value by adding the following JVM argument in the ColdFusion or JRun Administrator (depending on how you have installed ColdFusion). You must add the argument with the leading "-", preceded by a space, if there are already other JVM arguments present, as follows:
-DentityExpansionLimit=128000
You must decide what the new argument value will be. I simply used 128000 to double the default value.
For this change to take effect, you must restart ColdFusion. If cf_extremeQueryCache
continues to throw the error, try a larger value for the argument, restart
ColdFusion, and test it again until you eliminate the error.