mcpField
C
caching

caching

github.com/thinkoodle開発7187 回インストール8 週間前に更新
リポジトリ

caching について

Rails 開発者やコーディングエージェント向けのキャッシュ実装・診断スキルです。キャッシュ戦略、無効化、HTTP 条件付きリクエスト、ストア、テストを扱い、キャッシュ範囲より正しさを優先します。

主な機能

  1. 01HTTP、フラグメント、コレクション、低レベルキャッシュの選択
  2. 02キャッシュキーと無効化チェーンの設計
  3. 03Rails キャッシュストアの設定
  4. 04ETag と条件付き GET の利用
  5. 05キャッシュ動作のテストと古いデータの診断

活用シーン

  1. 01繰り返し描画されるパーシャルのキャッシュ
  2. 02高コストな計算や API 応答のキャッシュ
  3. 03レコード更新時の入れ子フラグメント無効化
  4. 04変更のないリソースへの 304 応答

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill caching

インストール後、Claude Code で/cachingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
cache-stores.md
conditional-get.md
fragment-caching.md
low-level-caching.md
performance.md
russian-doll.md

詳細

Rails Caching Expert

Rails アプリケーションで、高速かつ正確なキャッシュを実装します。キャッシュの適用範囲よりも正確性を優先してください。古いデータを表示するより、処理が遅いほうがましです。

哲学

基本原則:

  1. まずキャッシュの正確性を確保する — キャッシュミスは処理が遅くなるだけですが、古いデータはバグです
  2. 無効化が最も難しい — キャッシュを追加するたびに、無効化も必要になります
  3. キャッシュする前に計測する — 遅くないものはキャッシュしないでください
  4. Rails 8 のデフォルトは Solid Cache — 特別な理由がない限り、これを使用してください
  5. フラグメントキャッシュが基本 — 低レベルキャッシュではなく、ここから始めてください

キャッシュの階層(上にあるものほど優先):

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

低レベルキャッシュの主なルール:

  1. AR レコードには cache_key_with_version を使用します — レコードが更新されると、自動的に無効になります
  2. expires_in を設定します — 有効期限のないキャッシュには古いデータが蓄積し、デバッグが困難になります
  3. 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_keyproducts/1安定したキー(バージョンなし)
cache_key_with_versionproducts/1-20260101120000キーとタイムスタンプのバージョン
cache_version20260101120000バージョンのタイムスタンプのみ
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、ウォームアップ、スタンピードの防止、デバッグ

コメント