Programming log - Shindo200

イベント参加記録とプログラミング系の雑記

Rails のフラグメントキャッシュで使われるキーについて

試したバージョン: Rails 4.1.9

Railsにはテンプレートの一部をキャッシュ化する「フラグメントキャッシュ」という機能があります。使い方は『Ruby on Rails Guides』の「Caching with Rails: An overview — Ruby on Rails Guides」という記事と、『ASCIIcasts』の「ASCIIcasts - “Episode 387 - Cache Digests”」という記事で詳しく説明されていますので、興味がある方はそちらをぜひ読んでみてください。フラグメントキャッシュにはキャッシュ名を指定する方法が色々とあるのですが、キャッシュ名の指定の仕方によってキャッシュキーがどのような値になるのか疑問を持ちましたので、ソースコードを追いながら調べてみました。

【よちよち向け】フラグメントキャッシュとは

念のため、フラグメントキャッシュという機能について簡単に説明します。「フラグメントキャッシュを知ってる」という方は、この項目は読み飛ばしてください。

例えば、あるテンプレートに下記のコードが書いてあるとします。

<% PostalCode.all.each do |postal_code| %>
  <article>
    <h1><%= postal_code.zip_code %></h1>
    <div><%= postal_code.address %></div>
  </article>
<% end %>

「PostalCode」というクラス名が見えますので、おそらく郵便番号と住所を一覧で表示している部分なのでしょう。このビューを呼び出すと、下記のSQLが発行されます。

SELECT * FROM postal_codes; 

postal_codesテーブルからレコードを全件取得するだけのシンプルなSQLです。このSQLは、このテンプレートを呼び出すたびに発行されます。サービス運用中に郵便番号が増えたり、変更されることはそんなに多くはないのに、余計な手間を掛けている気がします。どうしましょうか。郵便番号をDBに保存することを止めて、テンプレートに直接書いてしまう?いや、それは良くないですね。

その問題を解決する方法の一つが「テンプレートのキャッシュ化」です。これは、テンプレートを呼び出したときにキャッシュストアにキャッシュされたテンプレートがあるならば、キャッシュからその部分が描画されるという機能になります。

Railsではcacheメソッドを使うことでテンプレートをキャッシュ化することができます。

<% cache 'cache_name' do %>
  <% PostalCode.all.each do |postal_code| %>
    <article>
      <h1><%= postal_code.zip_code %></h1>
      <div><%= postal_code.address %></div>
    </article>
  <% end %>
<% end %>

cacheメソッドのブロックの中がキャッシュ対象になります。このテンプレートを初めて呼び出したときはキャッシュがありませんのでPostalCodeを取得するSQLが発行されますが、2回目以降の呼び出しではSQLが発行されません。development環境でこの動作を確認したい方は、下記のように設定を変更してみてください。

# config/environments/development.rb
config.action_controller.perform_caching = true

キャッシュストアから該当のキャッシュを探すときは「キャッシュキー」という値が使われます。キャッシュキーはキャッシュ名の指定の仕方やテンプレートの内容によって変わってきます。同じキャッシュキーを持つキャッシュがキャッシュストアにあったときは、そのキャッシュが呼び出されます。このキャッシュキーが今回のエントリの主役になります。

キャッシュ名を渡さなかったとき

下記のように、キャッシュ名を渡さなかったときのキャッシュキーについてを調べてみました。

<% cache do %>
  <h1>Context menus</h1>
  <!-- ... -->
<% end %>

キャッシュ名を渡さなかったときは、内部ではActionDispatchのurl_forメソッドが呼び出されます。

ActionDispatchのurl_forメソッド

url_forメソッドの引数には空のハッシュが渡されます。url_forメソッドに空のハッシュを渡すと、ホスト名を含んだパスが返ってきます。このパスがキャッシュキーになります。

キャッシュ名を渡さなかったときのキャッシュキー

views/[ホスト名を含んだパス]/[ダイジェストキー]

例.

cache
# => キャッシュキー 
# views/example.com/foo/pipe9ec02fe17a9717c900b8f2490e92

キャッシュ名にActiveRecordのオブジェクトを渡したとき

下記のように、キャッシュ名にActiveRecordのオブジェクトを渡したときのキャッシュキーについてを調べてみました。

<!-- event変数にはActiveRecordのオブジェクトが入っていること -->
<% cache event do %>
  <h1>All the topics on this event</h1>
  <%= render event.topics %>
<% end %>

キャッシュ名にActiveRecordのオブジェクトを渡したときは、内部ではActiveRecordのcache_keyメソッドが呼び出されます。

ActiveRecordのcache_keyメソッド

cache_keyメソッドの返り値にはいくつかのパターンがありますが、大抵は"#{モデル名}/#{オブジェクトのID}-#{オブジェクトのupdated_at}"という形式の文字列が返ってきます。この文字列がキャッシュキーになります。

キャッシュ名にActiveRecordのオブジェクトを渡したときのキャッシュキー

views/[モデル名]/[オブジェクトのID]-[オブジェクトのupdate_at]/[ダイジェストキー]

例.

cache Event.find(1)
# => キャッシュキー
# views/events/1-20150201050000/pipe9ec02fe17a9717c900b8f2490e92

キャッシュ名に文字列を渡したとき

キャッシュ名に文字列を渡すと、その文字列がそのままキャッシュ名になります。

例.

cache 'foo/bar'
# => キャッシュキー
# view/foo/bar/pipe9ec02fe17a9717c900b8f2490e92

すごく単純ですね。

ダイジェストキーについて

キーの末尾に「pipe9ec02fe17a9717c900b8f2490e92」といった文字と数字の並びが挿入されています。これが「ダイジェストキー」です。ダイジェストキーは「Cache digests」というRails4から標準で組み込まれているGemによって作られているそうです。さて、このダイジェストキーの値は何に依存して決まるのでしょうか。

ドキュメントを読んだところ、フラグメントキャッシュを使っているテンプレート自体の内容と、そのテンプレートから呼び出している部分テンプレートの内容に依存してダイジェストキーが決まるそうです。Railsのログを見ながら、この動きを確認してみます。

# app/views/events/index.html.erb
<%= cache do %>
  <div>こんにちは, Ruby.</div>
<% end %>

このテンプレートで生成されるダイジェストキー

Cache digest for app/views/events/index.html.erb: 9b8dec5ce002410ce20272cb55824181

内容を少し書き換えてみます。

$ sed -i '' -e s/こんにちは/こんばんは/ app/views/events/index.html.erb

再度アクセスして、今回のテンプレートで生成されるダイジェストキーを確認してみます。

Cache digest for app/views/events/index.html.erb: 078e8f08f1d27ae2407d55750da69cc4

内容を少し変えただけで、ダイジェストキーが変わっていることがわかりました。この動きを見ると、確かにキャッシュを呼び出しているテンプレートの内容がダイジェストキーに関わっていそうですね。