キャッシュとサイドバー

某社の皆さんにも確認したのですが、やはり blog のサイドバーのような(公開されている)すべてのページに含まれる動的アイテムは partial や helper と before_filter などで用意するのが一般的なようです。以前存在した components という考え方は Rails 2.0 への移行時に非推奨になった様子。

個人的に helper や view にデータ操作を入れたくないので before_filter との合わせ技を採用。「表側」のコントローラに一枚専用のコントローラをかませます。

class PublicController < ActionController
  before_filter :setup_view_items

  def setup_view_items
    @categories = Category.find(:all)
  end
end

表で使うコントローラはこの PublicController を継承すれば、どこからでも上で取得した @categories を使いまわせます。後は view の方に partial を仕込むだけ。

<%= render :partial => '/common/public_categories', :collection => [@categories] %>

さて、ここで問題になるのはサイドバーは圧倒的に参照される回数が更新される回数を上回る(上回ってほしい)という点。毎度データを引っ張ってくるのも問題なので、キャッシュ(Fragment)を使います。

まずは先ほどの PublicController を変更。

class PublicController < ActionController
  before_filter :setup_view_items

  def setup_view_items
    @categories = Category.find(:all) unless read_fragment(
      :controller => 'public',
      :action => 'view_item',
      :action_suffix => 'public_categories')
  end
end

先ほど before_filter を採用したのも実はこの read_fragment を使いたかったからで、これを設定しておくことによりキャッシュの存在時はそちらを使うように変更されます。パラメータの指定はキャッシュしておくアイテムが複数になった時にどのキャッシュを使うのか判断するためのもので url_for の仕組みが使われるようです。

続いてビューの部分も変更。

<%- cache(:controller => 'public', :action => 'view_item', :action_suffix => 'public_categories') do -%>
  <%= render :partial => '/common/public_categories', :collection => [@categories] %>
<%- end -%>

先ほど同様、どのキャッシュを使うのかパラメータで指定しておきます。ちなみにサイドバーのように複数コントローラをまたぐ場合、きちんとパラメータ指定をしないとその時使用しているコントローラとアクションが自動的に指定され、結果的に複数のキャッシュが作られてしまうようなので注意。

ここまで完了したら動作を確認します。development 環境の場合は RAILS_ROOT/config/environments/development.rb を開いて以下の部分を編集します。

# config.action_controller.perform_caching = false
# true に変更
config.action_controller.perform_caching = true

その後サーバを再起動し、ログを確認(Rails 2.3.2 with mongrel でチェック)。

Cached fragment hit: views/localhost:3000/public/view_item?action_suffix=public_categories (0.0ms)

2回目以降のアクセスでは、確かにキャッシュが参照されました。しかしこのままではキャッシュが永遠に破棄されないので、データ更新時にキャッシュを破棄するよう変更します。

キャッシュの破棄にはコントローラ内で指定を行う方法もあるのですが、コントローラごとに書いていくのも面倒かつ ActiveScaffold のようなプラグインを使っている場合内側を書き換えないといけないので Sweeper を採用します。RAILS_ROOT/app/models 内に以下のように Sweeper を定義。

class ViewItemSweeper < ActionController::Caching::Sweeper
  observe Category

  def after_save(record)
    expire_fragment(:controller => '/public', :action => 'view_item', :action_suffix => 'public_categories')
  end
end

observe に監視対象のモデル(複数定義時はカンマで区切る)を書いて、後は通常のコールバックメソッドのように特定動作後に呼び出されるメソッドを定義。expire_fragment で破棄したいキャッシュをパラメータつきで指定します。
なお :controller の指定が 'public' ではなく '/public' なのは私がこれを RAILS_ROOT/app/controllers/admin という一つ深い階層から呼んでいたからで、こうしないと admin 以下のキャッシュを探そうとしてしまいます。この辺は url_for に準拠。
また、コールバック同様に update や create の後にも動作を行うのであれば、そのためのメソッドも定義しなくてはいけません。

class ViewItemSweeper < ActionController::Caching::Sweeper
  observe Category

  def after_save(record)
    expire_fragment(:controller => '/public', :action => 'view_item', :action_suffix => 'public_categories')
  end

  def after_create(record)
    expire_fragment(:controller => '/public', :action => 'view_item', :action_suffix => 'public_categories')
  end
end

最後に、この sweeper を利用する宣言をコントローラ側に記述します。

class Admin::CategoriesController < AdminController
  layout 'admin'
  cache_sweeper :view_item_sweeper, :only => [:create, :update, :destroy]

  active_scaffold :category
end

管理者側のコントローラなので認証用のコントローラを継承したり ActiveScaffold を使ったりしていますが、要は cache_sweeper の部分が重要。使い方は大体見てのとおりなのでわかると思います。

後はこんな感じで、表示したいアイテムをどんどん追加していけばいいのではないでしょうか。