問題
Rails エンジニアなら一度はハマる現象。
Rails.cache
で Active Record オブジェクトをキャッシュする際、キャッシュしたは良いものの、毎回クエリが発行されてしまう場合があります。
例えば以下のコード。
Rails.cache.fetch(key) do
User.where('status = 1').limit(1000)
end
原因と解決策
Rails.cacheの落とし穴 の記事に分かりやすく原因と解決策が書かれているので引用します。
ActiveRecord は評価を遅延させます。@users = User.allとしてもすぐにSQLが発行されることはなく@users.eachなど発行するクエリが決まった段階でクエリ発行されるわけです。
結論に書いたケースだとUser::ActiveRecord_Relationのインスタンスがキャッシュに入り、インスタンスが評価された段階でSQLが発行されるため、結果としてキャッシュを使っても毎回SQLが発行されてしまうわけです
以下の用にto_aを使ってクエリを即時発行すればOK。
Rails.cache.fetch(key) do
User.where('status = 1').limit(1000).to_a
end
デメリット
しかし .to_a
や .all
を使ってキャッシュさせる場合、以下のようなデメリットがあります。
- DB のレコードやカラムに変更があった場合、キャッシュされたオブジェクトには適用されないため、不整合が起きる可能性がある
- 巨大なオブジェクトをキャッシュするのはコストがかかるので、メモリを埋め尽くす可能性がある
- 利用元で
ActiveRecord::Relation
特有のメソッド(where
など)が利用できなくなる
好ましい形
user_ids = Rails.cache.fetch(key) do
User.where('status = 1').limit(1000).pluck(:id)
end
@user = User.where(id: user_ids)
上記のデメリットを回避したい場合、id などの変更されない値をキャッシュすることを考えます。
この場合、 User.where(id: user_ids)
は常に実行されますが、主キーを使用したクエリなので処理は高速です。これにより、先に説明したデメリットを回避しつつキャッシュを有効活用できます。
参考リンク
Confusion caching Active Record queries with Rails.cache.fetch