Rails Misapprehensions: Caching Views is Not the View’s Job!

{{{
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.
}}}

Advertisement

15 thoughts on “Rails Misapprehensions: Caching Views is Not the View’s Job!

  1. 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.

    Like

  2. @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!

    Like

  3. 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?

    Like

  4. @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 πŸ™‚

    Like

  5. @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…

    Like

  6. 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.

    Like

  7. @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:

    Like

  8. 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

    Like

  9. Formatting mess sorry

    The sweeper

    class SpotSweeper < ActionController::Caching::Sweeper
      observe Spot
    
      def after_save(record)
        [:edito, :header, :menu].each do |state|
          expire_cell_state SpotCell, state, record.spot_area.identifier
        end
      end
    end
    

    Like

  10. and the test

    it "should clear the cache" do
      render_cell(:spot, :edito, :area => "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
    

    Like

  11. Cyrille: Maybe try to let Cells compute the cache key?

    it "should clear the cache" do
      key = cell(:spot).
        state_cache_key(:edito, :homepage)
    

    Does that help? However, we should add a test method to make this easier!

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s