Caching is a generic approach to speed up processing time in software. A common pattern used in Rails is to *cache already rendered markup to save rendering time*.
Basically, there are two concepts for view caching in Rails.
* *Page caching* saves the complete page. This is good if your page doesn’t have dynamic parts.
* *Fragment caching* which caches only parts of the page – usually, a more complex approach since you have to manage caching for multiple fragments.
h3. Fragment caching
Rails supports fragment caching *by letting you define cached parts in the view*. You’d use the @#cache@ method for that.
All items in your cart: "items", :collection => current_user.items %>
This works fine, fragments get *rendered only if there’s a cache miss*. Three problems with this approach.
* You’re *cluttering your views with caching* declarations. It _is_ handy to quickly define an area as cached, however, what if you suddenly change your cache key generation? You have to *work through your _views_ for a low-level task*.
* You *shift responsibilities* – the _wrapping_ view defines the cached fragment. *Instead of placing knowledge _into_ the fragment, you aggregate all informations needed for caching on the _outside_*.
* Since you usually need to adjust the cache key for each fragment, *you’re putting expiration logic into your views*. This is definitely a no-go in MVC.
Let’s see how caching works in “cells view components”:http://github.com/apotonick/cells, which has a much simpler approach.
h3. View caching in Cells
In cells there simply *is no fragment caching*. As cells _are_ fragments *caching happens on the class layer*.
class CartCell < Cell::Rails cache :show def show(items) @items = items render end
In order to render the item list “fragment” you’d *call @#render_cell@ in your controller view*.
All items in your cart:
*Zero knowledge about caching in the view* at all. Everything from cache key generation to actually marking fragments as cached *happens _inside_ the component*, which knows best about its caching needs.
h3. No fragment caching?
The first thing worth discussing here is: What if you want to *cache a _part_ of a cell, only?* A fragment of a fragment, so to say.
You’d *model this as another cell state*. Period.
Let’s assume the item list in the cart should *have a cache entry for each item*.
class CartCell < Cell::Rails #cache :show cache :item do |cell, item| item.id end def item(item) @item = item render end def show(items)
Now, the cell’s @show.erb@ view would call the @#item@ state.
You have in your cart. :item}, item) %>
Again, the *wrapping @show@ state doesn’t know anything about* the item view’s internal caching. Notice how we *use @render :state@ to invoke another cell state.*
h3. Using versioners
Ok, now, each item gets its very own cache entry. How does that work?
class CartCell < Cell::Rails cache :item do |cell, item| item.id end
* Every time the @#item@ state is invoked *the versioner block is called*.
* It receives the @item@ instance (passed from @render :state@).
* Now the *versioner computes a cache key* – the return value from the block is *appended to the state’s generic cache key*. In our example, we’d generate keys like @cells/cart/item/1@, and so on.
Next time the @#item@ state is invoked with _item no. “1”_ *it won’t get rendered again since its cached view is returned instead.*
h3. Expiring caches
*Expiring caches is one of the two real problems* in computer science. The simplest approach is timed expiries.
class CartCell 10.minutes do |cell, item| item.id end
Each *item view gets flushed after 10 minutes*. Things can be so simple.
Some people unlike me like *sweepers*. Cells ships with a sweeper method.
class ItemSweeper < ActionController::Caching::Sweeper observe Item def after_update(record) expire_cell_state CartCell, :item, record.id end
Just *use @expire_cell_state@ to swipe views item-wise from the cache*. Thanks, Joe, for this example.
Even *regular expression expiry works with @expire_fragment@!* I didn’t even know this exists.
This will *remove items from item group 999*, only – presuming these item ids start with 999.
h3. Test your caching!
There _are_ *people telling you _not_ to test your caching*. These people either have *bullet-proof integration tests, or are just lame.* There is no excuse for missing caching tests.
I’d *refactor the versioner to an instance method first*, in order to unit-test.
class CartCell < Cell::Rails cache :item, :item_versioner def item_versioner(item) item.id end
Yeah, both blocks and methods work with @cache@. Let’s test that.
class CartCellCachingTest < Cell::TestCase test "item versioner appends id" do assert_equal "1", cell(:cart).item_versioner(@one) end
Testing if caching actually works is another good idea.
class CartCellCachingFunctionalTest < Cell::TestCase setup do ActionController::Base.perform_caching = true ActionController::Base.cache_store.clear @cell = cell(:cart) end teardown do ActionController::Base.perform_caching = false end
*Enable caching and always clear the cache store* – it’s good practice.
h3. Testing cache writes
We should also check whether *rendering a certain state alters the cache*.
class CartCellCachingFunctionalTest < Cell::TestCase test "rendering item should write to cache" do expected_key = @cell.class.state_cache_key(:item, 1) assert_equal "cells/cart/item/1", expected_key
First, I assert we got correct keys.
assert_not Cell::Base.cache_store.read(expected_key) render_cell(:cart, :item, @one) assert Cell::Base.cache_store.read(expected_key) end
Then I actually test if we got a cache write.
I agree that these tests are *pretty much bound to the caching implementation* in Cells and Rails. Also, I miss the ability to test cache reads and I’m waiting for inspiration from the community.
What could *help writing real caching tests could be an @#assert_caches@* or so. Let’s wait for reactions to this post.
h3. Get OOP back to your views!
Ok, now we learned how simple Cells’ state caching is.
To sum things up: The previous examples have – at least – two cool advantages in comparison to fragment caching.
* Although we cache fragments, we still have *any caching knowledge in the class* and not cluttered over the views itself.
* Our *”fragment” is an object-oriented component* – you could inherit from the @CartCell@ and “override the view”:http://nicksda.apotomo.de/2010/12/pragmatic-rails-thoughts-on-views-inheritance-view-inheritance-and-rails-304/comment-page-1/#comment-2584 while keeping the caching mechanics. This is definitly something the Rails community – used to monolithic controllers – *still has to discover* and I absolutely agree that this is an evolving process.
However, I hope this post helps you when it comes to caching views. Don’t forget to check out “the docs”:http://rdoc.info/gems/cells/Cell/Caching/ClassMethods#cache-instance_method as well.