Thursday, October 4, 2007

XML fragment caching in Rails

Rails has been nice to use. The most powerful part of it is the templating engine for RHTML and RXML. A problem that exists is that they do not share some of the same functionality. Both can execute Ruby code, and output specific content, access helpers, etc. Yet, RXML lacks the ability for fragment caching.

The problem exists for fragment caching because it was especially designed for ERB fragments. ERB allows Ruby code to be use as a templating language -- the same way PHP does for HTML. The nice thing out ERB is that everything evaluated and rendered is plain text, but the caching relies on simple tricks with Ruby's eval to get the output of a block of ERB. The caching method cache (for views) even uses a function called cache_erb_fragment.

I use RXML alot, so the benefits of fragment caching should be available as they are for RHTML. Especially with the slow rendering time of the RXML documents (a future post). RXML relies on overloading of a method using method_missing to help create a DSL for an XML document, in short, Ruby code gets outputted to XML.

An Example RXML document:


directors = ['George Lukas','Steven Speilberg','Michael Bay']
xml.instruct!
xml.directors do
directors.each do |name|
xml.director :name=>name
end
end
With the output as:

<directors>
<director name="George Lukas"/>
<director name="Steven Speilberg"/>
<director name="Michael Bay"/>
</directors>
A very simple example, but lets pretend the each director is dynamically loaded and that there is more information like age, location, and the last film they made. Information that will not change much till a movie is made, they get older, or they move. Never know when a director's already render XML might be needed again.

It has actually proven to be quite a task to do XML caching. It appears to be a subject neglected in the Rails community -- any idea why? I have posted on forums here and here about the topic. I had some preliminary code with those posts too, but I was having problems.

I would like to give back my final results of XML caching. Something that I hope is useful to some.


The following is the code use for the xml caching:

module ActionView
module Helpers
module CacheHelper
def cache_xml(name ={}, &block)
@controller.cache_xml_fragment(block, name)
end
end
end
end

module ActionController
module Caching
module Fragments
def cache_xml_fragment(block, name = {}, options = nil)
unless perform_caching then block.call; return end
buffer = eval("xml.target!",block.binding)
if cache = read_fragment(name, options)
buffer.concat(cache)
else
pos = buffer.length
block.call
write_fragment(name,buffer[pos..-1], options)
end
end
end
end
end
It works just like normal caching in RHTML documents. This is an example that can be used with the directors XML provided above.

Example Usage:

directors = ['George Lukas','Steven Speilberg','Michael Bay']
xml.instruct!
xml.directors do
directors.each do |name|
cache_xml("director/#{name}") do
xml.director :name=>name
end
end
end
I hope to make this into a plugin very soon. Its easy to copy and paste for right now.

UPDATE: The plugin can be found here.

1 comment:

Matt Mower said...

Surprised more people haven't commented as this is useful functionality that, at least as of 2.3.5, still hasn't been superceded.

Many thanks for your plugin. I made one, trivial, change to support passing the options from cache_xml through to cache_xml_fragment so that I can pass an expiry for memcached.

This is definitely going to help us since our RSS feeds are slow as molasses right now!

Thanks again.

Matt