caching について
Rails 開発者やコーディングエージェント向けのキャッシュ実装・診断スキルです。キャッシュ戦略、無効化、HTTP 条件付きリクエスト、ストア、テストを扱い、キャッシュ範囲より正しさを優先します。
主な機能
- 01HTTP、フラグメント、コレクション、低レベルキャッシュの選択
- 02キャッシュキーと無効化チェーンの設計
- 03Rails キャッシュストアの設定
- 04ETag と条件付き GET の利用
- 05キャッシュ動作のテストと古いデータの診断
活用シーン
- 01繰り返し描画されるパーシャルのキャッシュ
- 02高コストな計算や API 応答のキャッシュ
- 03レコード更新時の入れ子フラグメント無効化
- 04変更のないリソースへの 304 応答
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill cachingインストール後、Claude Code で/cachingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/cache-stores.mdconditional-get.mdfragment-caching.mdlow-level-caching.mdperformance.mdrussian-doll.md詳細
Rails Caching Expert
Rails アプリケーションで、高速かつ正確なキャッシュを実装します。キャッシュの適用範囲よりも正確性を優先してください。古いデータを表示するより、処理が遅いほうがましです。
哲学
基本原則:
- まずキャッシュの正確性を確保する — キャッシュミスは処理が遅くなるだけですが、古いデータはバグです
- 無効化が最も難しい — キャッシュを追加するたびに、無効化も必要になります
- キャッシュする前に計測する — 遅くないものはキャッシュしないでください
- Rails 8 のデフォルトは Solid Cache — 特別な理由がない限り、これを使用してください
- フラグメントキャッシュが基本 — 低レベルキャッシュではなく、ここから始めてください
キャッシュの階層(上にあるものほど優先):
HTTP Caching (stale?/fresh_when) ← Fastest: never even hits your app
Fragment Caching (cache helper) ← Fast: skips view rendering
Collection Caching (cached: true) ← Fast: batch cache reads
Low-Level Caching (Rails.cache) ← Flexible: cache any computation
SQL Caching (automatic per-request) ← Free: Rails does this for you
このスキルを使う場面
- ビュー、パーシャル、またはコストの高い計算にキャッシュを追加するとき
- キャッシュストア(Solid Cache、Redis、Memcached)を設定するとき
- 適切な無効化を伴うロシア人形キャッシュを実装するとき
- HTTP 条件付き GET(ETag、Last-Modified)を追加するとき
- 古いキャッシュやキャッシュ無効化の問題をデバッグするとき
- キャッシュ戦略でパフォーマンスを最適化するとき
手順
ステップ 1:キャッシュする対象を特定する
キャッシュのコードを書く前に、ボトルネックを特定してください。
# Check logs for slow queries or renders
grep "Completed" log/development.log | sort -t= -k2 -rn | head -20
# Use rack-mini-profiler or Rails server timing
# Look for: slow partials, N+1 queries, expensive computations
キャッシュに適した対象:
- ループ内でレンダリングされるパーシャル(商品カード、コメント一覧)
- コストの高い計算(レポート、集計、API 呼び出し)
- 変更頻度の低いコンテンツ(ナビゲーション、フッター、設定)
- 外部 API のレスポンス
キャッシュに適さない対象:
- リクエストごとに変わるコンテンツ(CSRF トークン、フラッシュメッセージ)
- ユーザーごとに異なるのに、ユーザー単位のキーを使っていないコンテンツ
- 適切にキーを設定せずに、認証が必要なコンテンツ
- データベースに書き込みを行うもの
ステップ 2:適切なキャッシュ戦略を選ぶ
| 戦略 | 用途 | キャッシュキーの基準 |
|---|---|---|
| フラグメントキャッシュ | ビューの HTML の一部をキャッシュする | レコード+テンプレートのダイジェスト |
| コレクションキャッシュ | パーシャルのコレクションをレンダリングする | 各レコード個別 |
| ロシア人形キャッシュ | 無効化を共有するネストされたフラグメント | touch を使ったレコード階層 |
| 低レベルキャッシュ | 計算結果や API 呼び出しをキャッシュする | 定義したカスタムキー |
| 条件付き GET | レスポンス全体に変更がない | ETag または Last-Modified |
ステップ 3:フラグメントキャッシュを実装する
基本的なフラグメントキャッシュ:
<% cache product do %>
<%= render product %>
<% end %>
Rails は次のようなキーを生成します:views/products/show:abc123/products/1-20260101120000000000
キーには次の情報が含まれます:
- テンプレートツリーのダイジェスト(テンプレートを変更すると変わります)
- レコードの
cache_key_with_version(レコードを更新すると変わります)
条件付きキャッシュ:
<% cache_if !current_user&.admin?, product do %>
<%= render product %>
<% end %>
ユーザーまたはロケールごとに異なるコンテンツ用の複合キャッシュキー:
<% cache [current_user, I18n.locale, product] do %>
<%= render product %>
<% end %>
ユーザーごとにコンテンツが変わる場合は、キャッシュキーにユーザーを含めてください。 含めないと、「Welcome, Ryan」をキャッシュして、すべての訪問者に表示してしまいます。これは最もよくあるキャッシュの間違いです。
ステップ 4:リストにはコレクションキャッシュを使う
ループ内で各項目をキャッシュするのではなく、コレクションキャッシュを使います。
<%# SLOW — one cache read per item %>
<% @products.each do |product| %>
<% cache product do %>
<%= render partial: "products/product", locals: { product: product } %>
<% end %>
<% end %>
<%# FAST — batch cache read (multi-fetch) %>
<%= render partial: "products/product", collection: @products, cached: true %>
コレクションキャッシュは、1 回のラウンドトリップですべてのキャッシュエントリーを読み込みます。大規模なコレクションでは、これにより大幅に高速化できます。
コレクション用のカスタムキャッシュキー:
<%= render partial: "products/product",
collection: @products,
cached: ->(product) { [I18n.locale, product] } %>
ステップ 5:ロシア人形キャッシュを実装する
ロシア人形キャッシュとは、内側のキャッシュ破棄が touch によって外側へ伝播する、ネストされたキャッシュフラグメントです。
ビュー:
<% cache @category do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
モデル — touch: true はキャッシュ無効化をチェーンの上位へ伝播します。
class Category < ApplicationRecord
has_many :products
end
class Product < ApplicationRecord
belongs_to :category, touch: true
end
商品を更新すると → その updated_at が変わり → touch: true によってカテゴリーの updated_at が更新され → 両方のキャッシュフラグメントが期限切れになります。
touch: true がないと、外側のキャッシュは古いデータを返します。 商品を更新してもカテゴリーの updated_at が変わらないため、カテゴリーのフラグメントは期限切れになりません。ロシア人形のチェーンに含まれるすべての belongs_to に touch: true が必要です。
複数レベルの touch チェーン:
class Store < ApplicationRecord
has_many :categories
end
class Category < ApplicationRecord
belongs_to :store, touch: true
has_many :products
end
class Product < ApplicationRecord
belongs_to :category, touch: true
# Updating a product touches category, which touches store
end
ステップ 6:Rails.cache による低レベルキャッシュ
fetch を使用します。1 回の呼び出しで読み取りと書き込みを処理できます:
class Product < ApplicationRecord
def competing_price
Rails.cache.fetch("#{cache_key_with_version}/competing_price", expires_in: 12.hours) do
Competitor::API.find_price(id)
end
end
end
低レベルキャッシュの主なルール:
- AR レコードには
cache_key_with_versionを使用します — レコードが更新されると、自動的に無効になります expires_inを設定します — 有効期限のないキャッシュには古いデータが蓄積し、デバッグが困難になります- Active Record オブジェクトではなく、ID またはプリミティブ値をキャッシュします — キャッシュされた AR オブジェクトは古くなり、コードの再読み込み時に問題が発生します
# Avoid — cached AR objects go stale and break on code reloads
Rails.cache.fetch("super_admins", expires_in: 12.hours) do
User.super_admins.to_a
end
# Better — cache IDs, re-query fresh
ids = Rails.cache.fetch("super_admin_ids", expires_in: 12.hours) do
User.super_admins.pluck(:id)
end
User.where(id: ids)
その他のキャッシュ操作:
Rails.cache.read("key") # Returns nil on miss
Rails.cache.write("key", value, expires_in: 1.hour)
Rails.cache.delete("key")
Rails.cache.exist?("key")
Rails.cache.increment("counter")
Rails.cache.decrement("counter")
# Delete by pattern (not supported by all stores)
Rails.cache.delete_matched("products/*")
# fetch_multi for batch operations
Rails.cache.fetch_multi("key1", "key2", "key3", expires_in: 1.hour) do |key|
expensive_computation(key)
end
ステップ 7:HTTP 条件付き GET(stale? / fresh_when)
最も効果の大きいキャッシュです。レスポンスが生成されること自体を防ぎます。
独自のレスポンスロジックがある場合は stale? を使用します:
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
if stale?(@product)
respond_to do |format|
format.html
format.json { render json: @product }
end
end
# If fresh, Rails auto-sends 304 Not Modified
end
end
シンプルなケース(デフォルトのテンプレートレンダリング)では fresh_when を使用します:
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
fresh_when @product
# That's it — renders template if stale, 304 if fresh
end
def index
@products = Product.all
fresh_when @products
end
end
オプションを明示的に指定する場合:
fresh_when last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version
変更されることのないコンテンツの場合:
def show
http_cache_forever(public: true) do
render
end
end
条件付き GET を使用する場面:
- 個々のレコードの表示ページ
- 更新パターンを予測できるインデックスページ
- クライアントがレスポンスをキャッシュする API エンドポイント
- 静的に近いページ(会社概要、利用規約など)
ステップ 8:キャッシュストアを設定する
Solid Cache(Rails 8 のデフォルト — こちらを使用)
SSD を使用するデータベースベースのキャッシュです。追加のインフラは必要ありません。
# config/environments/production.rb
config.cache_store = :solid_cache_store
# config/database.yml
production:
primary:
<<: *default
database: storage/production.sqlite3
cache:
<<: *default
database: storage/production_cache.sqlite3
migrations_paths: db/cache_migrate
# config/cache.yml
default: &default
store_options:
max_age: <%= 60.days.to_i %>
max_size: <%= 256.megabytes %>
namespace: <%= Rails.env %>
Solid Cache を使用する理由:
- インフラが不要です。データベースを使うだけです
- RAM ベースのストアより大きなストレージ容量を利用できます(SSD は安価です)
- 設定可能な max_age による FIFO 方式の削除に対応しています
- 機密性の高いキャッシュデータの暗号化に対応しています
- 水平スケーリングのためのシャーディングに対応しています
その他のストア: Redis(:redis_cache_store)、Memory(:memory_store)、Null(:null_store)。エラーハンドリングを含む Redis の本番環境向け設定と、各ストアの詳細な比較については references/cache-stores.md を参照してください。
ステップ 9:開発環境でキャッシュを有効にする
bin/rails dev:cache # Toggle caching on/off
これにより tmp/caching-dev.txt が作成または削除され、perform_caching が切り替わります。
開発環境で Solid Cache を使用する場合:
# config/environments/development.rb
config.cache_store = :solid_cache_store
また、開発環境の config/database.yml にキャッシュデータベースを追加します。
ステップ 10:キャッシュの動作をテストする
Rails のキャッシュ機能ではなく、キャッシュが正しく動作することをテストします:
class ProductTest < ActiveSupport::TestCase
test "competing_price is cached" do
product = products(:widget)
# First call hits the API
assert_equal 29.99, product.competing_price
# Verify cache was written
assert Rails.cache.exist?("#{product.cache_key_with_version}/competing_price")
end
test "touch propagation invalidates parent cache" do
category = categories(:electronics)
product = products(:widget)
old_updated_at = category.updated_at
product.update!(name: "New Name")
assert_operator category.reload.updated_at, :>, old_updated_at
end
end
リクエストテストでは、条件付き GET を検証します:
class ProductsControllerTest < ActionDispatch::IntegrationTest
test "returns 304 when product unchanged" do
product = products(:widget)
get product_path(product)
assert_response :success
etag = response.headers["ETag"]
get product_path(product), headers: { "HTTP_IF_NONE_MATCH" => etag }
assert_response :not_modified
end
end
よくある間違い
1. ユーザーキーを指定せずにユーザー固有のコンテンツをキャッシュする
<%# Wrong: all users see the same cached content %>
<% cache @dashboard do %>
Welcome, <%= current_user.name %>!
<%= render @dashboard.widgets %>
<% end %>
<%# Fixed: include user in cache key %>
<% cache [current_user, @dashboard] do %>
Welcome, <%= current_user.name %>!
<%= render @dashboard.widgets %>
<% end %>
2. 関連付けの touch がない
# Problem: updating a comment doesn't expire the post cache
class Comment < ApplicationRecord
belongs_to :post
end
# Fixed: touch propagates invalidation to parent
class Comment < ApplicationRecord
belongs_to :post, touch: true
end
3. Active Record オブジェクトをキャッシュする
# Problem: cached AR objects become stale and break on code reload
Rails.cache.fetch("featured") { Product.featured.to_a }
# Fixed: cache IDs, re-query fresh
ids = Rails.cache.fetch("featured_ids", expires_in: 1.hour) { Product.featured.pluck(:id) }
Product.where(id: ids)
4. 低レベルキャッシュに有効期限を設定しない
# Problem: cache lives forever, exchange rates go stale silently
Rails.cache.fetch("exchange_rates") { ExchangeRateAPI.current }
# Fixed: set expires_in so data refreshes
Rails.cache.fetch("exchange_rates", expires_in: 1.hour) { ExchangeRateAPI.current }
キャッシュキーの依存関係の問題やヘルパーのダイジェストに関する落とし穴など、その他のよくある間違いについては references/fragment-caching.md を参照してください。
クイックリファレンス:キャッシュキーメソッド
| メソッド | 戻り値 | 用途 |
|---|---|---|
cache_key | products/1 | 安定したキー(バージョンなし) |
cache_key_with_version | products/1-20260101120000 | キーとタイムスタンプのバージョン |
cache_version | 20260101120000 | バージョンのタイムスタンプのみ |
to_param | "1" | AR 以外のオブジェクト用のフォールバック |
リファレンス
詳細なパターン、エッジケース、高度な設定については、references/ ディレクトリを参照してください:
references/fragment-caching.md— キャッシュキー、複合キー、コレクションキャッシュ、依存関係references/russian-doll.md— ネストされたキャッシュ、touch の伝播、カウンターキャッシュreferences/low-level-caching.md— Rails.cache.fetch、fetch_multi、アトミック操作references/conditional-get.md— stale?、fresh_when、ETag、HTTP キャッシュreferences/cache-stores.md— ストアの比較、Solid Cache、Redis、Memcached の設定references/performance.md— キャッシュ呼び出しの N+1、ウォームアップ、スタンピードの防止、デバッグ