タケユー・ウェブ日報

Ruby on Rails や Flutter といった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