本ガイドでは、キャッシュを導入してRailsアプリケーションを高速化する方法を解説します。
「キャッシュ(caching)」とは、リクエスト・レスポンスのサイクルの中で生成されたコンテンツを保存しておき、次回同じようなリクエストが発生したときのレスポンスでそのコンテンツを再利用することを指します。
多くの場合、キャッシュはアプリケーションのパフォーマンスを効果的に増大するのに最適な方法です。キャッシュを導入することで、単一サーバーや単一データベースのWebサイトでも、数千ユーザーの同時接続による負荷に耐えられるようになります。
Railsには、すぐ利用できるキャッシュ機能がいくつも用意されています。本ガイドでは、それぞれの機能について目的を解説します。Railsのキャッシュ機能を使いこなすことで、応答時間の低下や高額なサーバー使用料に悩まされずに、Railsアプリケーションが数百万ビューを配信できるようになります。
このガイドの内容:
ここでは、キャッシュの手法を3種類ご紹介します。「ページキャッシュ」「アクションキャッシュ」「フラグメントキャッシュ」です。Railsのフラグメントキャッシュは本体に組み込まれており、デフォルトで利用できます。ページキャッシュやアクションキャッシュを利用するには、Gemfile
にactionpack-page_caching
gemやactionpack-action_caching
gemを追加する必要があります。
Action Controllerのキャッシュは、デフォルトではproduction環境でのみ有効になります。ローカルでキャッシュを使ってみたい場合は、対応する環境のconfig/environments/*.rb
ファイルでconfig.action_controller.perform_caching
をtrue
に設定します。
config.action_controller.perform_caching
値の変更は、Action Controllerコンポーネントで提供されるキャッシュでのみ有効です。つまり、後述する低レベルキャッシュの動作には影響しません。
Railsのページキャッシュは、apacheやnginxなどのWebサーバーによって生成されるページへのリクエストを、Railsスタック全体を経由せずにキャッシュするメカニズムです。ページキャッシュはきわめて高速ですが、どんな場面でも有効とは限りません。たとえば、認証の必要なページにはキャッシュが適用されません。また、Webサーバーはファイルシステムから直接ファイルを読み出して配信するので、キャッシュを失効させる機能の実装も必要です。
ページキャッシュ機能は、Rails 4で本体から削除されてgem化されました。actionpack-page_caching gemを参照してください。
ページキャッシュは、before_filter
のあるアクション(認証の必要なページなど)には適用できません。アクションキャッシュは、このような場合に使います。アクションキャッシュの動作は、ページキャッシュと似ていますが、WebサーバーへのリクエストがRailsスタックに到達したときに、before_filter
を実行してからキャッシュを配信する点が異なります。これによって、認証などの制限をかけながらキャッシュを配信できるようになります。
アクションキャッシュ機能は、Rails 4から削除されました。詳しくはactionpack-action_caching gemを参照してください。推奨される新しい方法については、DHH's key-based cache expiration overviewを参照してください。
通常、動的なWebアプリケーションではさまざまなコンポーネントを用いてページをビルドしますが、コンポーネントごとのキャッシュ特性は同じではありません。ページ内のさまざまなパーツごとにキャッシュや有効期限を設定したい場合は、フラグメントキャッシュを利用できます。
フラグメントキャッシュを使うと、ビューのロジックのフラグメントをキャッシュブロックでラップし、次回のリクエストでそれをキャッシュストアから取り出して配信します。
たとえば、ページ内で表示する製品(product)を個別にキャッシュしたい場合は、次のように書けます。
<% @products.each do |product| %> <% cache product do %> <%= render product %> <% end %> <% end %>
Railsアプリケーションが最初のリクエストを受信すると、一意のキーを持つ新しいキャッシュエントリが保存されます。生成されるキーは次のようなものになります。
views/products/index:bea67108094918eeba42cd4a6e786901/products/1
キーの途中にある文字列は、テンプレートツリーのダイジェストです。これは、キャッシュするビューフラグメントのコンテンツを元に算出されたハッシュダイジェストです。ビューフラグメントが変更されると(HTMLが変更されるなど)、このダイジェストも変更されて既存のファイルが無効になります。
productレコードから派生するキャッシュバージョンは、キャッシュエントリに保存されます。 productが変更されるとキャッシュバージョンが変更され、以前のバージョンを含むキャッシュフラグメントはすべて無視されます。
Memcachedなどのキャッシュストアは、古いキャッシュファイルを自動削除します。
条件を指定してフラグメントをキャッシュしたい場合は、cache_if
やcache_unless
を利用できます。
<% cache_if admin?, product do %> <%= render product %> <% end %>
render
ヘルパーは、コレクションでレンダリングされた個別のテンプレートもキャッシュできます。上のeach
によるコード例のようにキャッシュテンプレートを個別に読み出す代わりに、すべてのキャッシュテンプレートを一括で読み出すことも可能です。この機能を利用するには、コレクションをレンダリングするときに以下のようにcached: true
を指定します。
<%= render partial: 'products/product', collection: @products, cached: true %>
これにより、前回までにレンダリングされたすべてのキャッシュテンプレートが一括で読み出され、劇的に速度が向上します。しかも、それまでキャッシュされていなかったテンプレートもキャッシュに追加され、次回のレンダリングでまとめて読み出されるようになります。
キャッシュのキーはカスタマイズ可能です。以下のコード例では、productページでローカライズ結果が別のローカライズで上書きされないようにするため、現在のロケールをキャッシュにプレフィックスしています。
<%= render partial: 'products/product', collection: @products, cached: ->(product) { [I18n.locale, product] } %>
別のフラグメントキャッシュの内側にフラグメントをキャッシュしたいことがあります。このようにキャッシュをネストする手法を、マトリョーシカ人形のイメージになぞらえて「ロシアンドールキャッシュ」(Russian doll caching)と呼びます。
ロシアンドールキャッシュのメリットは、たとえば内側のフラグメントで製品(product)が1件だけ更新された場合に、内側の他のフラグメントを捨てずに再利用し、外側のフラグメントは通常どおり再生成できることです。
前節で解説したように、キャッシュされたファイルは、そのファイルが直接依存しているレコードのupdated_at
の値が変わると失効しますが、そのフラグメント内でネストしたキャッシュは失効しません。
次のビューを例に説明します。
<% cache product do %> <%= render product.games %> <% end %>
上のビューをレンダリングした後、次のビューをレンダリングします。
<% cache game do %> <%= render game %> <% end %>
gameのいずれかの属性で変更が発生すると、updated_at
値が現在時刻で更新され、キャッシュが無効になります。しかし、productオブジェクトのupdated_at
は変更されないので、productのキャッシュは無効にならず、アプリケーションは古いデータを配信します。これを修正したい場合は、次のようにtouch
メソッドでモデル同士を結びつけます。
class Product < ApplicationRecord has_many :games end class Game < ApplicationRecord belongs_to :product, touch: true end
touch
をtrue
に設定すると、あるgameレコードのupdated_at
を更新するアクションを実行したときに、関連付けられているproductのupdated_at
も同様に更新して、キャッシュを無効にします。
パーシャルのキャッシュや関連付けのキャッシュをMIMEタイプの異なる複数のファイルで共有できます。たとえば、パーシャルキャッシュを共有すると、テンプレートのライターがHTMLとJavaScript間でパーシャルキャッシュを共有できるようになります。テンプレートリゾルバのファイルパスに複数のテンプレートがある場合は、テンプレート言語の拡張子のみが含まれ、MIMEタイプは含まれません。これによって、テンプレートを複数のMIMEタイプで利用できます。HTMLリクエストとJavaScriptリクエストは、いずれも以下のコードにレスポンスを返します。
render(partial: "hotels/hotel", collection: @hotels, cached: true)
上のコードはhotels/hotel.erb
という名前のファイルを読み込みます。
別の方法は、レンダリングするパーシャルで以下のようにformats
属性を指定することです。
render(partial: "hotels/hotel", collection: @hotels, formats: :html, cached: true)
上のコードは、ファイルのMIMEタイプにかかわらずhotels/hotel.html.erb
という名前のファイルを読み込み、たとえばJavaScriptファイルでこのパーシャルをインクルードできるようになります。
キャッシュを正しく無効にするには、キャッシュの依存関係を適切に定義する必要があります。多くの場合、Railsでは依存関係が適切に処理されるので、特別な対応は不要です。ただし、カスタムヘルパーでキャッシュを扱うなどの場合は、明示的に依存関係を定義する必要があります。
テンプレートの依存関係は、ほとんどの場合テンプレート自身で呼び出されるrender
から導出されます。デコード方法を取り扱うActionView::Digestor
でrender
を呼び出す方法の例を以下にいくつか示します。
render partial: "comments/comment", collection: commentable.comments render "comments/comments" render "comments/comments" render("comments/comments") render "header" # render("comments/header")に変換される render(@topic) # render("topics/topic")に変換される render(topics) # render("topics/topic")に変換される render(message.topics) # render("topics/topic")に変換される
ただし、一部の呼び出しについては、キャッシュが適切に動作するための変更が必要です。たとえば、独自のコレクションを渡す場合は、次のように変更する必要があります。
render @project.documents.where(published: true)
上のコードを次のように変更します。
render partial: "documents/document", collection: @project.documents.where(published: true)
テンプレートの依存関係を自動的に導出できないことがあります。以下のようなヘルパー内でのレンダリングが典型的な例です。
<%= render_sortable_todolists @project.todolists %>
このような呼び出しでは、次の特殊なコメント形式で明示的に依存関係を示す必要があります。
<%# Template Dependency: todolists/todolist %> <%= render_sortable_todolists @project.todolists %>
単一テーブル継承(STI)などでは、こうした明示的な依存関係を多数書かなければならなくなる可能性もあります。テンプレートごとに依存関係を書く代わりに、以下のようにワイルドカードを用いてディレクトリ内の任意のテンプレートにマッチさせることも可能です。
<%# Template Dependency: events/* %> <%= render_categorizable_events @person.events %>
コレクションのキャッシュで、パーシャルの冒頭でクリーンなキャッシュ呼び出しを行わない場合は、以下の特殊コメント形式をテンプレートの任意の場所に追加することで、コレクションキャッシュを引き続き有効にできます。
<%# Template Collection: notification %> <% my_helper_that_calls_cache(some_arg, notification) do %> <%= notification.name %> <% end %>
たとえば、キャッシュされたブロック内でヘルパーメソッドを利用すると、このヘルパーを更新するときにキャッシュも更新しなければならなくなります。キャッシュの更新方法はさほど問題ではありませんが、テンプレートファイルのMD5を変更しなければなりません。推奨されている方法の1つは、以下のようにコメントで明示的に更新を示すことです。
<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %> <%= some_helper_method(person) %>
ビューのフラグメントをキャッシュするのではなく、特定の値やクエリ結果だけをキャッシュしたいことがあります。Railsのキャッシュメカニズムは、シリアライズ可能な任意の情報をキャッシュに保存するのに適しています。
低レベルキャッシュの最も効果的な実装方法は、Rails.cache.fetch
メソッドを利用することです。このメソッドは、キャッシュの書き込みと読み出しの両方に対応しています。引数を1個だけ渡すと、キーを読み出し、キャッシュから値を取り出して返します。ブロックを渡すと、キャッシュにヒットしなかった場合にブロックが実行されます。ブロックの戻り値は、指定のキャッシュキーの配下にあるキャッシュに書き込まれます。キャッシュにヒットした場合は、ブロックを実行せずにキャッシュの値を返します。
次の例を考えてみましょう。アプリケーションにProduct
モデルがあり、競合Webサイトの製品価格を検索するインスタンスメソッドがそのモデルにあるとします。このメソッドが返すデータは、低レベルキャッシュに最適です。
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
上の例ではcache_key_with_version
メソッドを使っているので、キャッシュキーはproducts/233-20140225082222765838000/competing_price
のような形式になります。cache_key_with_version
は、モデルのクラス名とid
とupdated_at
属性を元に文字列を生成します。この生成ルールは一般的に使われており、productが更新されるたびにキャッシュが無効になるというメリットがあります。一般に、低レベルキャッシュを適用する場合、キャッシュキーを生成する必要があります。
以下のコード例で考えてみましょう。このコードでは、スーパーユーザーを表すActive Recordオブジェクトのリストをキャッシュに保存しています。
# super_adminsのSQLクエリは重いので頻繁に実行しないこと Rails.cache.fetch("super_admin_users", expires_in: 12.hours) do User.super_admins.to_a end
このようなパターンは、インスタンスが変更される可能性があるので、避けてください。production環境では、インスタンスの属性が変わるかもしれませんし、レコードが削除される可能性もあります。development環境でも、コードを変更すると再読み込みするキャッシュストアの挙動は信頼できません。
インスタンスをキャッシュするのではなく、以下のようにidなどのプリミティブなデータ型をキャッシュするようにしましょう。
# super_adminsのSQLクエリは重いので頻繁に実行しないこと ids = Rails.cache.fetch("super_admin_user_ids", expires_in: 12.hours) do User.super_admins.pluck(:id) end User.where(id: ids).to_a
Railsのクエリキャッシュは、各クエリが返す結果セットをキャッシュする機能です。リクエストによって以前と同じクエリが発生すると、データベースへのクエリを実行する代わりに、キャッシュされた結果セットを利用します。
以下に例を示します。
class ProductsController < ApplicationController def index # 検索クエリの実行 @products = Product.all ... # 同じクエリの再実行 @products = Product.all end end
データベースに対して同じクエリが再度実行されると、実際にはデータベースにアクセスしません。1回目のクエリでは、結果をメモリ上のクエリキャッシュに保存し、2回目のクエリではメモリから結果を読み出します。
ただし、キャッシュはアクションの実行中しか保持されないという点が重要です(クエリキャッシュはアクションの開始時に作成され、アクションの終了時に破棄されます)。クエリ結果をより長期間保存したい場合は、低レベルキャッシュを利用できます。
Railsには、キャッシュデータの保存場所がいくつも用意されています。なお、SQLキャッシュやページキャッシュはこの中に含まれません。
アプリケーションのデフォルトのキャッシュストアは、config.cache_store
オプションで設定できます。キャッシュストアのコンストラクタには、引数として他のパラメータも渡せます。
config.cache_store = :memory_store, { size: 64.megabytes }
または、設定ブロックの外部でActionController::Base.cache_store
を設定することも可能です。
キャッシュにアクセスするには、Rails.cache
を呼び出します。
:mem_cache_store
と:redis_cache_store
は、デフォルトではプロセスごとに1つのコネクションを利用します。これは、Puma(または別のスレッド化サーバー)を使えば、複数のスレッドがキャッシュストアへのクエリを同時実行できるということです。
コネクションプールを無効にしたい場合は、キャッシュストアの設定時に:pool
オプションをfalse
に設定します。
config.cache_store = :mem_cache_store, "cache.example.com", { pool: false }
また、:pool
オプションに個別のオプションを指定することで、デフォルトのプール設定をオーバーライドすることも可能です。
config.cache_store = :mem_cache_store, "cache.example.com", { pool: { size: 32, timeout: 1 } }
:size
: プロセス1個あたりのコネクション数を指定します(デフォルトは5)
:timeout
: コネクションごとの待ち時間を秒で指定します(デフォルトは5)。タイムアウトまでにコネクションを利用できない場合は、Timeout::Error
エラーが発生します。
ActiveSupport::Cache::Store
ActiveSupport::Cache::Store
は、Railsでキャッシュとやりとりするための基盤を提供します。これは抽象クラスなので、単体では利用できません。代わりに、ストレージエンジンと結びついたこのクラスの具象実装を使う必要があります。Railsにはいくつかの実装が同梱されており、ドキュメントは以下にあります。
主要なAPIメソッドは、read
、write
、delete
、exist?
、fetch
です。
キャッシュストアのコンストラクタに渡されるオプションは、該当するAPIメソッドのデフォルトオプションとして扱われます。
ActiveSupport::Cache::MemoryStore
ActiveSupport::Cache::MemoryStore
は、エントリーを同じRubyプロセス内のメモリに保持します。
キャッシュストアのサイズを制限するには、イニシャライザで:size
オプションを指定します(デフォルトは32MB)。キャッシュがこのサイズを超えるとクリーンアップが開始され、直近の利用が最も少ない(LRU: Least Recently Used)エントリから削除されます。
config.cache_store = :memory_store, { size: 64.megabytes }
Ruby on Railsサーバーのプロセスを複数実行している場合(Phusion PassengerやPumaをクラスタモードで利用している場合)は、Railsサーバーのキャッシュデータをプロセスのインスタンス間で共有できなくなります。このキャッシュストアは、アプリケーションを大規模にデプロイするには向いていません。ただし、小規模でトラフィックの少ないサイトでサーバープロセスを数個動かす程度であれば問題なく動作します。もちろん、development環境やtest環境でも動作します。
新規Railsプロジェクトのdevelopment環境では、この実装をデフォルトで使うよう設定されます。
:memory_store
を使うとキャッシュデータがプロセス間で共有されないため、Railsコンソールから手動でキャッシュを読み書きすることも無効にすることもできません。
ActiveSupport::Cache::FileStore
ActiveSupport::Cache::FileStore
は、エントリをファイルシステムに保存します。キャッシュを初期化するときに、ファイル保存場所へのパスを指定する必要があります。
config.cache_store = :file_store, "/path/to/cache/directory"
このキャッシュストアを使うと、同一ホスト上にある複数のサーバープロセス間でキャッシュを共有できるようになります。トラフィックが中規模程度のサイトを1、2個程度ホストする場合に向いています。異なるホストで実行するサーバープロセス間のキャッシュを共有ファイルシステムで共有することも一応可能ですが、おすすめできません。
キャッシュはディスクが満杯になるまで増加するため、古いエントリを定期的に削除することをおすすめします。
config.cache_store
を明示的に指定しない場合は、デフォルトのキャッシュストア実装("#{root}/tmp/cache/"
)が提供されます。
ActiveSupport::Cache::MemCacheStore
ActiveSupport::Cache::MemCacheStore
は、アプリケーションキャッシュの保存先をDangaのmemcached
サーバーに一元化します。Railsでは、本体にバンドルされているdalli
gemがデフォルトで使われます。dalli
は、現時点で最も広くproduction Webサイトで利用されているキャッシュストアです。高性能かつ高冗長性を備えており、単一のキャッシュストアも共有キャッシュクラスタも提供できます。
キャッシュを初期化するときは、クラスタ内の全memcachedサーバーのアドレスを指定するか、MEMCACHE_SERVERS
環境変数を適切に設定しておく必要があります。
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
どちらも指定されていない場合は、memcachedがlocalhostのデフォルトポート(127.0.0.1:11211
)で実行されていると仮定しますが、これは大規模サイトのセットアップには向いていません。
config.cache_store = :mem_cache_store # $MEMCACHE_SERVERSにフォールバックし、次に127.0.0.1:11211になる
サポートされているアドレスの種類について詳しくはDalli::Client
のドキュメントを参照してください。
このキャッシュのwrite
メソッド(およびfetch
メソッド)は、memcached固有の機能を利用する追加オプションを受け取れます。
ActiveSupport::Cache::RedisCacheStore
ActiveSupport::Cache::RedisCacheStore
は、メモリ使用量が最大に達したときにRedisの自動eviction(立ち退き)を利用して、Memcachedキャッシュサーバーと同様の機能を実現しています。
デプロイに関するメモ: Redisのキーはデフォルトでは無期限なので、専用のRedisキャッシュサーバーを使うときはご注意ください。永続化用のRedisサーバーに期限付きのキャッシュデータを保存してはいけません。詳しくはRedis cache server setup guide(英語)を参照してください。
「キャッシュのみ」のRedisサーバーでは、maxmemory-policy
に以下のいずれかのallkeysを設定してください。
Redis 4以降ではallkeys-lfu
によるLFU(Least Frequently Used: 利用頻度が最も低いキャッシュを削除する)evictionアルゴリズムがサポートされており、これはデフォルトの選択肢として非常によいものです。
Redis 3以前では、allkeys-lru
を用いてLRU(Least Recently Used: 直近の利用が最も少ないキャッシュを削除する)アルゴリズムにすべきです。
キャッシュの読み書きのタイムアウトは、やや低めに設定しましょう。キャッシュの取り出しで1秒以上待つよりも、キャッシュ値を再生成する方が高速になることもよくあります。読み書きのデフォルトタイムアウト値は1秒ですが、ネットワークのレイテンシが常に低い場合は値を小さくするとよい結果が得られることがあります。
キャッシュストアがリクエスト中に接続に失敗した場合、デフォルトではRedisへの再接続を1回試みます。
キャッシュの読み書きでは決して例外が発生せず、単にnil
を返してあたかも何もキャッシュされていないかのように振る舞います。キャッシュで例外が生じているかどうかを測定するには、error_handler
を渡して例外収集サービスにレポートを送信してもよいでしょう。収集サービスは、「method
(最初に呼び出されたキャッシュストアメソッド名、)」「returning
(ユーザーに返した値(通常はnil
)」「exception
(rescueされた例外)」の3つのキーワード引数を受け取れる必要があります。
Redisを利用するには、まずGemfile
にredis gemを追加します。
gem "redis"
最後に、関連するconfig/environments/*.rb
ファイルに以下の設定を追加します。
config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }
少し複雑なproduction向けRedisキャッシュストアは以下のような感じになります。
cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0) config.cache_store = :redis_cache_store, { url: cache_servers, connect_timeout: 30, # デフォルトは1(秒) read_timeout: 0.2, # デフォルトは1(秒) write_timeout: 0.2, # デフォルトは1(秒) reconnect_attempts: 2, # デフォルトは1 error_handler: -> (method:, returning:, exception:) { # エラーをwarningとしてSentryに送信する Sentry.capture_exception exception, level: "warning", tags: { method: method, returning: returning } } }
ActiveSupport::Cache::NullStore
ActiveSupport::Cache::NullStore
は個別のWebリクエストを対象とし、リクエストが終了すると保存された値をクリアするので、development環境やtest環境での利用のみを想定しています。Rails.cache
と直接やりとりするコードを使っていて、キャッシュが原因でコード変更の結果が反映されなくなる場合にこのキャッシュストアを使うと非常に便利です。
config.cache_store = :null_store
キャッシュストアを独自に作成するには、ActiveSupport::Cache::Store
を拡張して適切なメソッドを実装します。これにより、Railsアプリケーションでさまざまなキャッシュ技術に差し替えられるようになります。
カスタムのキャッシュストアを利用するには、自作クラスの新しいインスタンスにキャッシュストアを設定します。
config.cache_store = MyCacheStore.new
キャッシュで使うキーには、cache_key
とto_param
に応答する任意のオブジェクトが使えます。自分のクラスでcache_key
メソッドを実装すると、カスタムキーを生成できるようになります。Active Recordは、このクラス名とレコードidに基づいてキーを生成します。
キャッシュのキーとして、値のハッシュと配列を指定できます。
# このキャッシュキーは有効 Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
Rails.cache
で使うキーは、ストレージエンジンで実際に使われるキーと同じになりません。実際のキーは、バックエンドの技術的制約に合わせて名前空間化または変更される可能性もあります。そのため、たとえばRails.cache
で値を保存してからdalli
gemで値を取り出すようなことはできません。その代わり、memcachedのサイズ制限超過や構文規則違反を気にする必要もありません。
条件付きGETは、HTTP仕様で定められた機能です。「GETリクエストへのレスポンスが前回リクエストのレスポンスから変更されていなければ、ブラウザ内キャッシュを安全に利用できる」とWebサーバーからブラウザに通知します。
この機能は、HTTP_IF_NONE_MATCH
ヘッダとHTTP_IF_MODIFIED_SINCE
ヘッダを使って、一意のコンテンツidや最終更新タイムスタンプをやり取りします。コンテンツid(ETag)または最終更新タイムスタンプがサーバー側のバージョンと一致する場合は、「変更なし」ステータスのみを持つ空レスポンスをサーバーが返すだけで済みます。
最終更新タイムスタンプやif-none-match
ヘッダの有無を確認して、完全なレスポンスを返す必要があるかどうかを決定するのは、サーバー側(つまり開発者)の責任です。Railsでは、次のように条件付きGETを比較的簡単に利用できます。
class ProductsController < ApplicationController def show @product = Product.find(params[:id]) # 指定のタイムスタンプやETag値によって、リクエストが古いことがわかった場合 # (再処理が必要な場合)、このブロックを実行する if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version) respond_to do |wants| # ... 通常のレスポンス処理 end end # リクエストがフレッシュな(つまり前回から変更されていない)場合は処理不要。 # デフォルトのレンダリングでは、前回の`stale?`呼び出しの結果に基いて # 処理が必要かどうかを判断して :not_modifiedを送信するだけでよい。 end end
オプションハッシュの代わりに、単にモデルを渡すことも可能です。Railsのlast_modified
やetag
の設定では、updated_at
メソッドやcache_key_with_version
メソッドが使われます。
class ProductsController < ApplicationController def show @product = Product.find(params[:id]) if stale?(@product) respond_to do |wants| # ... 通常のレスポンス処理 end end end end
特殊なレスポンス処理を使わずにデフォルトのレンダリングメカニズムを利用する(つまりrespond_to
も使わず独自レンダリングもしない)場合は、fresh_when
ヘルパーで簡単に処理できます。
class ProductsController < ApplicationController # リクエストがフレッシュな自動的に:not_modifiedを返す # 古い場合はデフォルトのテンプレート(product.*)を返す def show @product = Product.find(params[:id]) fresh_when last_modified: @product.published_at.utc, etag: @product end end
last_modified
とetag
が両方設定されている場合の振る舞いは、config.action_dispatch.strict_freshness
の値によって異なります。
true
に設定されている場合、RFC 7232セクション6で指定されているようにetag
のみが考慮されます。false
に設定されている場合、両方の条件が満たされていれば、キャッシュは最新のものと見なされます。これは、従来のRailsの振る舞いと同じです。静的ページなどの有効期限のないページでキャッシュを有効にしたいことがあります。http_cache_forever
ヘルパーを使うと、ブラウザやプロキシでキャッシュを無期限にできます。
キャッシュのレスポンスはデフォルトではprivateになっており、キャッシュはユーザーのWebブラウザでのみ行われます。プロキシでレスポンスをキャッシュ可能にするには、public: true
を設定してすべてのユーザーへのレスポンスがキャッシュされるようにします。
このヘルパーメソッドを使うと、last_modified
ヘッダーがTime.new(2011, 1, 1).utc
に設定され、expires
ヘッダーが100年に設定されます。
このメソッドの利用には十分ご注意ください。ブラウザやプロキシにキャッシュされたレスポンスは、ブラウザのキャッシュを強制的にクリアしない限り無効にできません。
class HomeController < ApplicationController def index http_cache_forever(public: true) do render end end end
Railsは、デフォルトで「弱い」ETagを使います。弱いETagでは、レスポンスのbodyが微妙に異なる場合にも同じETagを与えることで、事実上同じレスポンスとして扱えるようになります。レスポンスbodyのごく一部が変更されたときにページを再生成したくない場合に便利です。
弱いETagの冒頭にはW/
が追加されるので、強いETagと区別できます。
W/"618bbc92e2d35ea1945008b42799b0e7" → 弱いETag "618bbc92e2d35ea1945008b42799b0e7" → 強いETag
強いETagは、弱いETagと異なり、レスポンスがバイトレベルで完全一致しなければなりません。強いETagは巨大な動画やPDFファイル内でRangeリクエストを実行する場合に便利です。Akamaiなど一部のCDNでは、強いETagのみをサポートしています。強いETagの生成がどうしても必要な場合は、次のようにできます。
class ProductsController < ApplicationController def show @product = Product.find(params[:id]) fresh_when last_modified: @product.published_at.utc, strong_etag: @product end end
次のように、レスポンスに強いETagを直接設定することも可能です。
response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"
developmentモードでは、デフォルトで:memory_store
キャッシュが有効になります。
Railsにはdev:cache
コマンドも提供されているので、これを用いて以下のようにAction Controllerのキャッシュを手軽にオンオフできます。
$ bin/rails dev:cache Development mode is now being cached. $ bin/rails dev:cache Development mode is no longer being cached.
キャッシュを無効にするには、cache_store
を:null_store
に設定します。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。