Rails 7: caching?とuncachable!ヘルパーが追加(翻訳)
よく「キャッシュの無効化はコンピュータサイエンスの難問であり、バグを引き起こす可能性がある」と言われますが、実際、キャッシュすべきでないものをキャッシュするとバグやセキュリティ脆弱性の温床になることがあります。
Railsにはフラグメントキャッシュの仕組みが組み込まれていて、レンダリングされたビューの一部をフラグメントとして保存します。以後のリクエストでは、再レンダリングする代わりに保存済みのフラグメントが使われます。
ただしフラグメントキャッシュは深刻なバグを引き起こす可能性があります。たとえば、商品ごとに固有の署名済みURLを生成するAWS S3 URLヘルパーを使う場合や、リクエスト固有の認証トークンを出力するフォームヘルパーを書く場合を考えてみましょう。このような場合は、フラグメントキャッシュは避ける方がよいでしょう。
変更前
cache
ヘルパーを使ってフラグメントキャッシュを実装できます。
views/products/index.html.erb
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Image</th>
</tr>
</thead>
<tbody>
<% @products.each do |product| %>
<% cache(product) do %>
<%= render product %>
<% end %>
<% end %>
</tbody>
</table>
views/products/_product.html.erb
<tr>
<td><%= product.title %></td>
<td><%= product.description %></td>
<td><%= image_tag(generate_presigned_url(product.image_url)) %></td>
</tr>
しかしこのコードでは、毎回一意の署名済みURLを生成してもレンダリングのたびにキャッシュされた方の商品(product)が取得されてしまうというバグがあります。これを解決するには、productパーシャル内にcacheable
を書く必要があります。これで、誰かがproductパーシャルをキャッシュしようとすると、ActionView::Template::Error
エラーが発生します。
変更後
<tr>
<%= uncacheable! %>
<td><%= product.title %></td>
<td><%= product.description %></td>
<td><%= image_tag(generate_presigned_url(product.image_url)) %></td>
</tr>
上のようにuncacheable!
を書いておくと、以下のようになります。
ActionView::Template::Error (can't be fragment cached):
1: <tr>
2: <%= uncacheable! %>
3: <td><%= product.title %></td>
4: <td><%= product.description %></td>
5: <td><%= image_tag(generate_presigned_url(product.image_url)) %></td>
app/views/products/_product.html.erb:2
また、caching?
ヘルパーは、現在のコードパスがキャッシュされているかどうかのチェックや、キャッシュの強制に使えます。
<tr>
<%= raise Exception.new "This partial needs to be cached" unless caching? %>
<td><%= product.title %></td>
<td><%= product.description %></td>
</tr>
この変更に関する議論について詳しくは、#42365をご覧ください。
関連記事
Rails 7: ActiveRecord::Relationにstructurally_compatible?が追加(翻訳)
概要
原著者の許諾を得て翻訳・公開いたします。