Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

169 lines (122 sloc) 3.669 kB

Caching in Rails

Caching uses a fast, usually in-memory store, to persist data that takes a long time to retrieve or calculate. Caching identifies data by key and usually saves it for a given duration.

Page Caching

The WidgetsController makes two queries: one for widgets and another for gadgets that belong to each widget.

Processing by WidgetsController#index as HTML
  Widget Load (2.2ms)  SELECT "widgets".* FROM "widgets" 
  Gadget Load (0.5ms)  SELECT "gadgets".* FROM "gadgets" WHERE "gadgets"."widget_id" = 1
  Gadget Load (0.5ms)  SELECT "gadgets".* FROM "gadgets" WHERE "gadgets"."widget_id" = 2
  Rendered widgets/index.html.haml within layouts/application (171.0ms)
Completed 200 OK in 195ms (Views: 134.5ms | ActiveRecord: 43.9ms)

We can start by caching widgets.

class WidgetsController < ApplicationController

  def index
    @widgets = get_cached_widgets
  end

  def get_cached_widgets
    Rails.cache.fetch("widgets", :expires_in => 10.minutes) do
      Widget.all
    end
  end

end

Notice the query time in ActiveRecord reduced from 43.9ms to 9.0ms.

Processing by WidgetsController#index as HTML
  Gadget Load (8.3ms)  SELECT "gadgets".* FROM "gadgets" WHERE "gadgets"."widget_id" = 1
  Gadget Load (0.7ms)  SELECT "gadgets".* FROM "gadgets" WHERE "gadgets"."widget_id" = 2
  Rendered widgets/index.html.haml within layouts/application (33.9ms)
Completed 200 OK in 49ms (Views: 39.4ms | ActiveRecord: 9.0ms)

A similar effect can be accomplished with a caches_page helper after enabling perform_caching in environments/development.rb. This caches the entire page output.

config.action_controller.perform_caching = true
class WidgetsController < ApplicationController

  caches_page :index

  def index
    @widgets = Widget.all
  end

end

Cache Expiration

Caches can be set to expire.

Rails.cache.fetch("widgets", :expires_in => 10.seconds) do
  Widget.all
end

Or can be expired explicitly.

def create
  expire_page :action => :index
end

Action Caching

Caching pages cannot be combined with authentication, because filters are run within the cache block. To cache the output of an action, use action caching.

class WidgetsController < ApplicationController

  before_filter :authenticate!
  caches_action :index

  def index
    @widgets = Widget.all
  end

end

Fragment Caching

Specific portions of a page can be cached with fragment caching.

<% cache('all_widgets') do %>
  # ...
<% end %>

Sweepers

Instead of adding cache expiration logic into the controller, we can sweep the cache by implementing an observer.

class WidgetSweeper < ActionController::Caching::Sweeper
  observe Widget

  def after_create(widget)
    expire_cache_for(widget)
  end

  def after_update(widget)
    expire_cache_for(widget)
  end

  def after_destroy(widget)
    expire_cache_for(widget)
  end

  private

    def expire_cache_for(widget)
      expire_page(:controller => 'widgets', :action => 'index')
      expire_fragment('all_widgets')
    end

end

Testing

Enable caching in config/environments/test.rb.

config.action_controller.perform_caching = true

The following controller test expects Widget.all to be called only the first time the page is hit.

it "should cache :index action" do
  Widget.should_receive(:all).once
  2.times { get :index }
end

We also need to clear the Rails cache between runs.

config.before :each do
  Rails.cache.clear
end
Jump to Line
Something went wrong with that request. Please try again.