2013-10-30 (Wed) [長年日記]
_ ロシア人形キャッシュとキャッシュダイジェスト
Russian-doll Caching (Cache Digests) は、Rails 4 に導入された機能です。これは、複数のフラグメントキャッシュを入れ子にしている場合に、内側のコンテンツの更新に連動して、外側のフラグメントキャッシュのキーが自動的に変更されるというものです。
[Rails アプリケーションのパフォーマンスについて RubyKaigi 2013 で発表しましたより引用]
という記事を見て、内側のフラグメントキャッシュ内で使用されているモデルが更新される場合も、外側のキャッシュのキーが自動的に更新されるのかなと勘違いしたけど、そんなわけはなかったのでメモ。
ロシア人形キャッシュ
まず、ロシア人形キャッシュというのは、
<% cache @article do %> <h1><%= @article.title %></h1> <p><%= @article.body %></p> <h2>コメント</h2> <%= render @article.comments %> <% end %>
のようなフラグメントキャッシュがあった時に、キャッシュの中で使用しているテンプレート(comments/_comment.html.erb)でも
<% cache comment do %> <p><%= comment.body %></p> <p>by <%= comment.name %> at <%= comment.created_at %></p> <% end %>
のようにフラグメントキャッシュを使うこと。こうすると、外側のキャッシュが無効になった時も内側のキャッシュ(の一部)は有効なことが多いので、更新された場合をレンダリングするだけで済む。
モデル更新時のキャッシュの無効化
内側のキャッシュが無効になった場合は、外側のキャッシュを無効にする必要があるが、belongs_toのtouchオプションを使うと子モデルが更新された時に親モデルのupdated_atを自動的に更新できる(これはRails 3でも使える機能)。
class Article < ActiveRecord::Base has_many :comments, dependent: :destroy end class Comment < ActiveRecord::Base belongs_to :article, touch: true end
キャッシュのキーはupdated_atを元に生成されるので、updated_atが更新されると古いキャッシュは無効になる(ヒットしなくなる)。
テンプレート更新時のキャッシュの無効化
問題はテンプレートファイルを更新した時で、モデルのデータは変わっていなくても、キャッシュを無効にする必要がある。
従来は、
<% cache ["v1", comment] do %> … <% end %>
のようにcacheの引数にテンプレートのバージョンを含めて対応することができた。 フラグメントキャッシュは引数のオブジェクト全体からキャッシュキーを生成するので、commentの方に変更がなくても、"v1"の部分を変えてやればキャッシュキーが変わり、古いキャッシュが無効になる。 ただ、手動でやるのはいかにも面倒くさいし、このテンプレートに依存する他のテンプレート(この場合はarticles/show.html.erb)の方のバージョンを更新するのを忘れるリスクがある。
キャッシュダイジェスト
Rails 4では自動的にテンプレートファイルのダイジェスト値がキャッシュキーに追加されるのでこういったことをする必要がなくなった(これがキャッシュダイジェストという新機能)。 また、テンプレートの依存関係を把握していて、内側のテンプレートが変更された場合も外側のキャッシュのキーが自動的に変更される。
注意点
上記のように賢い機能だが、
<%= render @project.documents.where(published: true).order('created_at') %>
のような複雑なケースだと依存関係を自動では認識してくれないようなので注意が必要。
render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
のようにpartialオプションでテンプレートを明示したり、
<%# Template Dependency: documents/document %>
のようにコメントで依存関係を明示すれば大丈夫らしいけど、render partial:とか冗長だよねとか言ってリファクタリングしてもテストは通るからハマリそうな気がする(Railsってこういう罠が多い気が)。
あと、キャッシュデータそのものは勝手に消えるわけではないので、例えばMemCacheStoreを使っている場合は、以下のようにexpireする時間を指定するなどの工夫が必要になる*1。
config.cache_store = :mem_cache_store, "cache1.example.com", "cache2.example.com", {expires_in: 3.hours}
*1 Rails 4のMemCacheStoreではmemcache-clientの代りにDalliが使われるようになったのでexpireの指定ができる。