#387 Cache Digests
- Download:
- source codeProject Files in Zip (61 KB)
- mp4Full Size H.264 Video (16.2 MB)
- m4vSmaller H.264 Video (8.08 MB)
- webmFull Size VP8 Video (10.9 MB)
- ogvFull Size Theora Video (16.8 MB)
下の図は、サンプルアプリケーションのスクリーンショットです。何件かのプロジェクトの一覧が表示され、それぞれに複数のタスクが登録されています。例えばこのページにパフォーマンスの問題があり、速度向上のためにfragment cachingの機能を付加したいとしましょう。
このページのテンプレートを以下に示します。このファイルが単純にプロジェクトのページを描画します。
<h1>Projects</h1> <%= render @projects %>
各プロジェクトごとに、部分テンプレート_project
が描画されます。このテンプレートもとてもシンプルで、プロジェクトごとのタスクを描画します。
<h2><%= link_to project.name, edit_project_path(project) %></h2> <ul><%= render project.tasks %></ul>
この部分テンプレートは、プロジェクトに含まれる各タスクのためのもうひとつの部分テーンプレートの_task
を描画します。
<li> <%= task.name %> <%= link_to "edit", edit_task_path(task) %> </li>
部分テンプレートの_projects
にfragment cachingを付加します。fragment cachingについて知らないという場合は、エピソード90で詳しく説明しているので参照してください。
<% cache project do %> <h2><%= link_to project.name, edit_project_path(project) %></h2> <ul><%= render project.tasks %></ul> <% end %>
この部分テンプレートでタスクの情報を出力しているので、タスクが変更されたときにキャッシュが期限切れになるのが都合がいいでしょう。そのためにはTaskモデルを修正し、関連(association)のproject
にtouch: true
を追加します。
class Task < ActiveRecord::Base attr_accessible :name, :completed_at belongs_to :project, touch: true end
タスクが更新されたときに、そのタスクが属するプロジェクトに更新マークがつきます。効果を確認するために、設定ファイルのdevelopment.rbを修正し、perform_caching
をtrue
に設定します。
config.action_controller.perform_caching = true
Railsアプリケーションを再起動してトップページをリロードすると、プロジェクトの情報はfragment cacheに保存されます。ページをリロードする度ごとに、それらの部品がキャッシュから読み込まれます。タスクを編集すると、そのプロジェクトのキャッシュが期限切れになり、新規のものが作成されます。
キャッシュされたテンプレートの変更を処理する
これはうまくいきますが、ビューテンプレートを変更したらどうなるでしょうか? 例えばタスクを、unordered listではなく、ordered listで再描画したいとしましょう。
<% cache project do %> <h2><%= link_to project.name, edit_project_path(project) %></h2> <ol><%= render project.tasks %></ol> <% end %>
ページをリロードしても変化はありません。各タスクのHTMLはすでにキャッシュに保存されていて、キャッシュがまた期限切れになっていないので、古い内容がまだ表示されています。この問題を回避する一般的な方法として、キャッシュキーにバージョン番号を追加します。
<% cache ['v1', project] do %> <h2><%= link_to project.name, edit_project_path(project) %></h2> <ol><%= render project.tasks %></ol> <% end %>
cache keyを変更したので、キャッシュは期限切れになりリロードされ、タスクはordered listとして表示されます。
ネストされたキャッシュの処理
この方法の問題は、テンプレートを更新するたびに忘れずにバージョン番号を変更しなければいけません。それによってcache keyが変わり変更が反映されることになります。難しくはないですが、fragment cacheがネストされた場合にとたんに管理しきれなくなってしまいます。例えば、パフォーマンスを少しでも向上させるためにタスクの部分テンプレートをfragment cacheしたいとしましょう。
<% cache ['v1', task] do %> <li> <%= task.name %> <%= link_to "edit", edit_task_path(task) %> </li> <% end %>
そのためには、キャッシュの全体が期限切れになるようにプロジェクトの部分テンプレートのcache keyのバージョン番号も更新しなくてはいけません。ここでタスクの部分テンプレートを更新(例えばリンクの文字列をeditからrenameに変更)する場合、バージョン番号も更新する必要があります。ですが、プロジェクトの部分テンプレートのバージョン番号も更新しない限り、ページをリロードしてもこの変更は反映されません。両方を変更したときに初めて、変更が反映されます。
Cache Digest Gemを使う
うまくはいきましたが、テンプレートに修正を加えるたびに忘れずにこの変更を行うのは面倒です。そのような場合にCache Digests gemが役に立ちます。この機能はRails 4では標準で利用できますが、gemとして抽出されたので、Rails 3のアプリケーションでも利用可能です。このgemはテンプレートに基づくfragment cache keyにdigestを含むことによって機能します。つまりテンプレートが変更されると新しいcache keyが生成されてそのキャッシュは期限切れになります。このgemを実際のアプリケーションで試してみます。いつものようにgemfileに追加した後にbundleコマンドでインストールを行ないます。
gem 'cache_digests'
もうfragment cacheのバージョン番号の履歴を管理する必要がないので、projectとtaskの両方の部分テンプレートからそれらを削除します。アプリケーションを再起動してトップページをリロードすると、テンプレートを意味するcache keyにダイジェストが含まれます。
projectsのテンプレートを変更してページをリロードしても変更は反映されていないようです。これはcache digest gemが、毎回のリクエストでテンプレートファイルを読み込むのが効率が悪いのでそれを行わないことが原因です。その代わりにテンプレートパス毎にダイジェストのローカルキャッシュをメモリに保持するので、それが原因でテンプレートの変更が反映されていません。開発環境でこれを動作させるために、Railsアプリケーションを再度起動し直します。新しいテンプレートがデプロイされたらいずれにしろアプリケーションはいずれにしろ再起動されるので、これが本番環境で問題になることはまずありません。ページをリロードすると、キャッシュダイジェストが変わっったので、変更されたテンプレートが表示されています。
この機能の良い点の一つは、依存関係を自動的に検知してくれるという点です。例えばprojectsテンプレートにプロジェクトのタスクに対するrender
の呼び出しがあることを検知しているので、tasksの部分テンプレートが変更されたらprojectsのキャッシュを期限切れにします。
そうは言っても、依存性が正しく検知されない場合があることも考慮する必要があります。例えばProject
モデルにincomplete_tasks
メソッドがあります。これを使ってプロジェクトの部分テンプレートで未完了のタスク一覧を作成し、その後でタスク一覧の部分テンプレートを変更すると、依存性が検出されないため変更は反映されません。そこでgemが提供するcache_digests:nested_dependencies
というRakeタスクを実行してテンプレートへのパスを渡すのがいいでしょう。
$ rake cache_digests:nested_dependencies TEMPLATE=projects/index [ { "projects/project": [ "incomplete_tasks/incomplete_task" ] } ]
これによってわかるのは、projectの部分テンプレートの依存性が検出されて、これは正しいと判断され、それに加えてincomplete_task
があるのは正しくなく、正しくはtaskであるべきだということです。これは、依存性を反映するために変更しなくてはいけない部分です。このような場合には、以下のように描画するために、明示的にpartial
オプションを渡す方法が推奨されています。
<% cache project do %> <h2><%= link_to project.name, edit_project_path(project) %></h2> <ul><%= render partial: 'tasks/task', collection: project.incomplete_tasks %></ul> <% end %>
Rakeタスクを実行するとtasksの依存性が正しく検知されて、cache digestが正しく更新されます。
$ rake cache_digests:nested_dependencies TEMPLATE=projects/index [ { "projects/project": [ "tasks/task" ] } ]
gemのREADMEにはこれについての詳細情報が記載されており、じっくり読む価値があります。それによるとどのようなrenderの呼び出しが正しく解析されるかあるいはされないかがわかり、またヘルパーメソッドを使用して部分テンプレートを描画している場合にテンプレートの依存性を見つけるもう一つの方法が示されています。