Let's talk about caching a page that has a ton of database objects that need to be pulled. Specifically, because that page render is centered around a monster model that has way too many dependencies. This is going to be lengthy, so get yourself some coffee and settle in for an emotional rollercoaster.

Example

Alright, let's start off with an example. So let’s say you have a Rails project. And let’s say you have a “master” model all up in there. A “god” model if you will. It’s probably User. It’s User, isn’t it? Be honest. Anyway. You probably have a bunch of stuff in that user.rb file of yours. Something like

class User < ActiveRecord::Base
  has_one  :headshot
  has_many :articles

  # 500 more associations over here
end

and your app/views/user.html.slim file is riddled with cache blocks to look like

/ I want a cache block here for @user : ((
div 
  - cache [@user.headshot, :headshot] do
    = image_tag @user.headshot.image.small

  - cache [@user, :details] do
    h2 = user.name
    p = user.about_me

  - cache [@user.articles, :other_things] do
    @user.articles.each do |article|
      - cache [@article, :thumb_description]
        = image_tag @article.header_image
        = @article.short_description
/ etc

Which means that, while sure, you have some caching in there and the HTML renders will be faster, you're still making quite a few database calls to verify all those caches. You could solve this by adding belongs_to :user, touch:true to all of your models that are part of the above block, but then 1: that doesn't work because touch is not suported for all associations and 2: you're making db updates to user when you are updating unrelated objects. Also: this becomes quite involved for through associations.

This is where I sell you stuff. Specifically, I wrote a gem called La Maquina. This bit of code allows you to define arbitrary associations between models and notify about updates. This is a hard sentence to parse, so lemme show you some code. Using the example above for only article, we can set up our models as follows

class User < ActiveRecord::Base
  include LaMaquina::Notifier
  notifies_about :self

  has_many :articles
  # etc
end

class Article < ActiveRecord::Base
  include LaMaquina::Notifier
  notifies_about :user

  belongs_to :user
  # etc
end

# with all the other associations like headshot would be set up the same way as Article

This is the very basic of plumbing. If you were to examine the input into the LaMaquina engine at this point, you'd see the :self notification fire (conceptually) as "a user is notifying about user #{id}"; and for the article, you'd see "an article is notifying about user #{id}".

This is the interesting bit. Now that we have the notifications flowing, we have to have some code to process those. The way LaMaquina does this is with plugins called Pistons. They are code that take the caller and callee class names and then can process them in as simple or complex way as you need. Just as an example, here's what a piston that implements the old touch functinoality would look like.

class TouchPiston < LaMaquina::Piston::Base
  class << self
    # for Article, we'd get "user", user_id, "article"
    def fire!( notified_class, id, notifier_class = "" )
      # User
      updated_class = notified_class.camelize.constantize

      # User.find(id)
      object = updated_class.find(id)

      object.touch
    end
  end
end

While this is not recommended for cache invalidation (LaMaquina::Piston::CachePiston is probably what you want to use), this will allow you to update your slim to be more like this:

/ this is the important bit right here
- cache [@user, :profile]
  div
    - cache [@user.headshot, :headshot] do
      = image_tag @user.headshot.image.small

    - cache [@user, :details] do
      h2 = user.name
      p = user.about_me

    - cache [@user.articles, :other_things] do
      @user.articles.each do |article|
        - cache [@article, :thumb_description]
          = image_tag @article.header_image
          = @article.short_description

So now, so long as the user hasn't been touched, your page will render with a single db call. One. Isn't that exciting? I think that's pretty neat.

As a sidenote, you'll probably want to keep the inner cache blocks as they were, as they'll help when there are partial page rebuilds. Like, if a single article is added/updated, you won't want to rebuild the entire page.

Setup

Ok. So that's great. But I added all of this code and nothing is happening. What gives?

You need to do some minor setup. In your config/intializers, you'll need to add a la_maquina.rb that sets up all of this stuff. Something along the lines of

LaMaquina::Piston::CachePiston.redis = Redis::Namespace.new(:cache_piston, redis: Redis.new)
LaMaquina::Engine.install LaMaquina::Piston::CacheAssemblerPiston

LaMaquina.error_notifier = LaMaquina::ErrorNotifier::HoneybadgerNotifier

For a more thorough explanation of what all of that means, plz RTM.

Important note: if using CachePiston or your own custon cache key generator, don't forget to add a cache_key method to your target models

class User < ActiveRecor::Base
  def cache_key
    LaMaquina::Piston::CachePiston.cache_key(:user, id)
  end
end

otherwise rails will default to model/id/updated_at, which will of course ignore your shiny new key and you will be very sad.

Bonus round

So this is all great and you're using the CachePiston and all of you're views are blazing fast.

BUT WAIT, THERE'S MORE

So you know how you set up that piston to update your cache when a model changed? Well, you can add an arbitrary number of pistons that do all sorts of things. You're using Solr and want user to be reindexed when it's updated? You can do that (there's actually a proto-piston for that already). You want to fire Kafka notifications when articles get created? You can do that too. RSS? Why not. Push notifications? Sure! All kinds of things can happen. You can even rebuild the views on the backend if you want. The world is your oyster now.

You're welcome. Now go, make your app radical.