{{{
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
Step-wise.
* 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.
expire_fragment %r(cells/cart/item/999)
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.
}}}
Muy interesante
LikeLike
A little much with the bold no?
If anything this “cell” based approach seems 10x more complex and not Rails like than just using the cache helper. It’s the views job to cache content, I don’t see what is so hard to understand about that.
LikeLike
@Anony: The problems with Rails fragment caching are described pretty detailed in this post. Please, be so kind yourself, and be more precise when saying this “approach seems 10x more complex”, and explain what scares you.
I think the biggest problem is that Cells forces you to rethink your view architecture. While you could simply use a quick
cache
helper in the view Cells urges you to model that fragment into an object-oriented method. And this might look strange to some people, but it is the logical conclusion for a real MVC framework with components. Oi!LikeLike
Cool! Ruby5 features this article in the show #150. Thanks, Gregg and Nathan!
LikeLike
This has not generally been a problem for me, actually:
> 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.
My experience is generally that the including page knows better what kind of staleness it can tolerate when using time-based expiry. For example, my home page can tolerate a few minutes of staleness, while the admin UI can tolerate none.
Besides, can’t you just push the cache block down a level if you want the sub-view to have responsibility?
LikeLike
@Marcel: I see your points. In a typical Rails setup with a monolithic page, you wouldn’t need the fine-grained mechanics I describe in the post.
Nevertheless, keep in mind that a Cell shouldn’t be mapped to an entire page, but to single “blocks” on that page. And imo, a “Recent Comments” block knows best about its staleness, and not the surrounding page.
I like that you bring the Admin UI as an example. You have multiple “blocks” there with different caching needs for each of those – how do you handle that? A
#cache
block per “block”?You’re free to push things down to the partial – but do you really want that kind of “stacked” caching declarations?
BTW: The point you’re refering to is definitely the weakest in my argument list π
LikeLike
@nick in light of Marcel’s use case, is there a way to render a cell and explicitly bypass the cache? I.e. Could the cell have a 10 minute cache but the admin interface request a fresh copy anyway (and re-cache that)?
It’s an interesting point, but personally I find it hard to believe that you’d have the same forward facing (read: public) components as you would on your admin interface, anyway…
LikeLike
Nick,
This is great! Rails caching is something that is easy to get started with, but also easy to get tangled up in very quickly.
For people not familiar with Cells, and those the came over from the ruby5 podcast, you might have come to this post expecting to see some code snippet that is going to make your vanilla Rails site cache everything better.
But this is more of an example of how caching can be simpler when pulled out of the view, and the view is composed of small reusable components. More of a ‘food for thought’ post, than a how-to, but if you are about to go down the path of adding more complex caching in your app, you would do well to make it as simple as you can.
The more I see about Cells, the more I want to try it out.
LikeLike
@Bodanil: Sorry for the late answer. Cells – being good Rails citizens – comply with
config.perform_caching
so you could use that flag to disable caching in backends.I’m not a fan of this since you’re fiddling with a global configuration variable, but I’m sure it’s kinda easy to extend Cells to make them disable caching under a certain route or so. Interesting point, definitely!
@John: Thanks! You’re right, this ain’t no introducing post but a discussion focused on a different approach, so I understand people may be disappointed at first sight (“WTF- I don’t wanna use Cells for caching my Rails views?!”). I’m happy you like it!
This is all I remember:
LikeLike
Nice post ! Everything works as intended even after a “conversion” to rspec π
Just wondering how you will test the expire_cell_state in a sweeper such as:
class SpotSweeper “homepage”)
Cell::Base.cache_store.read(“cells/spot/edito/homepage”).should_not be_nil
# so far working…
# Call the sweeper and clean the cache
SpotSweeper.instance.after_save(@spot)
# test the cache has been wipe out
Cell::Base.cache_store.read(“cells/spot/edito/homepage”).should be_nil
end
However I’d no success so far. I can’t find a way to call a force the sweeper callback in my specs
LikeLike
Formatting mess sorry
The sweeper
LikeLike
and the test
LikeLike
Cyrille: Maybe try to let Cells compute the cache key?
Does that help? However, we should add a test method to make this easier!
LikeLike
This is not prudent.
LikeLike
This is like the “Russian Doll Caching” in Rails 4? Why didn’t Rails just use Cells??
LikeLike