タケユー・ウェブ日報

Webシステム受託会社の業務の中での気づきや調べごとのメモ。

コレクションキャッシュでキャッシュのキーをカスタマイズする方法

コレクションキャッシュについてコードリーディングを行ったところ、キャッシュキーのカスタマイズ方法がわかったのでメモ。

この記事のまとめ

Railsガイドには書いてありませんが、:cached に callable なオブジェクトを渡すと、その結果をキャッシュキーとして使ってくれます。

# cached: true と同じ
<%= render partial: 'posts/post', collection: @posts, cached: -> (post) { post } %>
# posts.comments の結果に変更があった場合も違うキーになる
<%= render partial: 'posts/post', collection: @posts, cached: -> (post) { [post, post.comments] } %>
# locals はキャシュキーに影響しないため、locals によってキーを変えるときは自分で指定してやる必要がある
<%= render partial: 'posts/post', collection: @posts, locals: { flag: true }, cached: -> (post) { [{flag: true}, post] } %>

github.com

コレクションキャッシュ

一覧などを表示する際に便利な :collection オプションは :cached オプションを渡すことで、各アイテムの描画でキャッシュを使ってくれます。

たとえば

<%= render partial: 'posts/post', collection: @posts, cached: true %>

とすると、ログは次のようになります。

Rendered collection of posts/_post.html.erb [0 / 10 cache hits] (Duration: 1733.3ms | Allocations: 2034040)
Rendered collection of posts/_post.html.erb [10 / 10 cache hits] (Duration: 1.2ms | Allocations: 546)

キャッシュのキー

キャッシュのキーは、ビューファイルのパス、内容のハッシュ値、コレクションアイテム(今回の場合Postインスタンスオブジェクト)による配列となります。(6.0.3現在)

# キャッシュのキー
[:views, "posts/_post:88c97eec416c8a7c8aaa37acb5edbbbd", #<Post id: 123, ... >]

rails/collection_caching.rb at b378bda17d710eb08949771390e7b7d77ee2b39d · rails/rails · GitHub

なお、配列のキーの場合、Railsのキャッシュ機構は配列要素から文字列のキーとキャッシュバージョンを取り出して使用します。(5.2から) 今回の場合、 Post#cache_version #=> updated_at から作った文字列 がキャッシュバージョンに含まれるため、 Post を更新すると新しいキャッシュとなります。

キャッシュのキーを変更したいケース

上記のような標準のキャッシュキーではうまくキャッシュを破棄できない場合があります。

コレクションアイテムの依存先の変更で描画内容を変化させたい

次のような Post#comments によって描画内容が変化するテンプレートの場合

<!-- posts/_post.html.erb -->
<h3><%= post.title %></h3>
<p><%= post.comments.count %> comments</p>

post.comments.count が変わってもにキャッシュキーが変化しないため、

<%= render partial: 'posts/post', collection: @posts, cached: true %>

ではキャッシュが入れ替わりません。

これを避けるためには、次のどちらかがよく採用されると思います。

  1. comments が変更される度に、 postupdated_at を更新してキャッシュバージョンを変化させる( post.touch
  2. キャッシュキーに comments も含める

1の方法はコールバックなどの仕組みが必要で、モデルの管理が難しくなるので、私は2の方が使いやすいと思います。

コレクションアイテム以外の入力で描画内容が変化するとき

次の例は、人気の記事と新着記事で同一のテンプレートを使い、一方では人気順のバッジを表示する、というものです。

<h2>人気の記事<h2>
<%= render partial: 'posts/post', collection: @popular_posts, locals: { show_rank_badge: true }, cache: true %>
<h2>新着記事</h2>
<%= render partial: 'posts/post', collection: @recent_posts, locals: { show_rank_badge: false }, cache: true %>

このような場合、たとえば @popular_posts@recent_posts に同じ記事が含まれていた場合、新着記事の表示欄でもバッジ付きの表示になってしまいます。 これを避けるためには、それぞれ違うキャッシュキーになるようにしなければいけません。

キャッシュのキーを変更する方法

ActionView::CollectionCaching のコードを確認すると次のようにしてキーを変更することができました。(Rails 6.0.3)

<%= render partial: 'posts/post', collection: @posts, cached: -> (post) { [post, post.comments] } %>
<%= render partial: 'posts/post', collection: @posts, locals: { show_rank_badge: true }, cached: -> (post) { [post, { show_rank_badge: true }] } %>

github.com