特定の値やクエリ結果をキャッシュすることを低レベルキャッシュと呼びます。
今回は低レベルキャッシュを実装する際に利用されるRails.cache
について紹介します。1
目次
Rails.cacheについて
RailsではActiveSupport::Cache::Storeを利用することでデータのキャッシュができます。
Rails.cache
とはRailsアプリケーションからActiveSupport::Cache::Store
を操作するメソッドです。
Rails.cacheによるデータ操作に関するメソッドについて
Rails.cache
の主要メソッドは以下の通りです。2
- Rails.cache.read
- Rails.cache.write
- Rails.cache.delete
- Rails.cache.exist?
- Rails.cache.fetch
以下では各メソッドについて紹介します。
Rails.cache.read
read
はキャッシュからデータを取得するメソッドです。
Rails.cache.read(key名)
Rails.cache.write
write
はキャッシュにデータを書き込むメソッドです。
Rails.cache.write(key名, 値)
具体例は以下の通りです。
### 書き込み
> Rails.cache.write('example_key', 'example_value')
=> "OK"
### 読み込み
> Rails.cache.read('example_key')
=> "example_value"
Rails.cache.delete
delete
はキャッシュからデータを削除するメソッドです。
Rails.cache.delete(key名)
具体例は以下の通りです。
### 読み込み
> Rails.cache.read('example_key')
=> "example_value"
### 削除
> Rails.cache.delete('example_key')
=> 1
### 削除したので結果がnilになる
> Rails.cache.read('example_key')
=> nil
Rails.cache.exist?
exist?
はキーの存在有無を確認するメソッドです。
Rails.cache.exist?(key名)
具体例は以下の通りです。
### 書き込み
> Rails.cache.write('example_key', 'example_value')
=> "OK"
### 存在しているのでtrue
Rails.cache.exist?('example_key')
=> true
### 存在していないのでfalse
Rails.cache.exist?('hoge')
=> false
Rails.cache.fetch
fetch
はキャッシュにデータが存在していればキャッシュからデータを取得し、存在していなければキャッシュにデータを書き込むメソッドです。
### キャッシュからデータを取得する
Rails.cache.fetch(kye名)
### キャッシュにデータが存在していれば取得、存在していなければ書き込み
Rails.cache.fetch(key名) do
# キャッシュ対象のデータ
end
Rails.cache.fetch
の実行例は以下の通りです。
### キャッシュからデータを取得。キーが存在していなければnilになる
> Rails.cache.fetch('example_key')
=> nil
### キーが存在していないのでブロックの評価値をキャッシュに保存
> Rails.cache.fetch('example_key') do
> 'example_value'
> end
=> "example_value"
#### キャッシュからデータを取得。キーが存在していればvalueが返ってくる
> Rails.cache.fetch('example_key')
=> "example_value"
Railsのキャッシュの保存先(キャッシュストア)の種類
設定できるキャッシュの保存場所は以下の通りです。3
- ファイルシステム
- メモリ
- memcached
- Redis
- そのほか(キャッシュストアの独自実装)
Rails.cache.class
でどの設定が利用されているかわかります。
たとえばRedisを利用している場合、Rails.cache.class
は以下のようにActiveSupport::Cache::RedisCacheStore
と表示されます。
### Redisを利用している場合
> Rails.cache.class
=> ActiveSupport::Cache::RedisCacheStore
キャッシュストアの設定方法
キャッシュストアの設定はconfig.cache_store
で行います。記述例は以下の通りです。2
### ファイルシステムの例
config.cache_store = :file_store, "/path/to/cache/directory"
### メモリの例
config.cache_store = :memory_store, { size: 64.megabytes }
### Memcachedの例
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
### Redisの例
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
### 独自実装の例
config.cache_store = MyCacheStore.new
Redisを利用する方法の詳細解説はRails.cacheの保存先(キャッシュストア)をRedisにする方法で紹介しているので参考にしてください。
Rails.cacheによるキーの自動生成メソッドについて
Rails.cache
で利用するキーは任意の文字列で問題ありません。
しかし、キャッシュを正しく利用するためには『データが更新されたタイミングでキャッシュを再作成する』『キーに対して取得できるデータは一意になるようにする』など、考慮すべきポイントがいくつかあります。
Rails.cache
にはキーを自動生成するメソッドが用意されています。Rails.cache
のメソッドを利用することでデータ更新に伴うキャッシュの再作成や、キーの重複を気にする必要がなくなります。ですので、特に理由がない限りはRails.cache
でキーを自動生成するとよいでしょう。
以下では各メソッドについて紹介します。
単一レコードに対するキーを生成するcache_key_with_version
cache_key_with_version
は『モデルのクラス名』『id』『updated_at』の情報を組み合わせたキーを作成するメソッドで、単一レコードをキャッシュする際に利用されます。
具体的なキーの文字列は以下のようになります。
> user = User.first
> user.cache_key_with_version
=> "users/1-20210717060310803913"
### レコードが異なるとキーも異なる
> user = User.second
> user.cache_key_with_version
=> "users/2-20210717060340840903"
### 同一レコードでも更新されるとキーが新しくなる
> user = User.first
> user.update(active: false)
> user.cache_key_with_version
=> "users/1-20210717060512672178"
実装では以下のような形で利用されます。1
class Product < ApplicationRecord
def competing_price
### キャッシュを利用することで外部APIを叩く頻度を減らしている
# expires_in: キャッシュの有効期限
Rails.cache.fetch("#{cache_key_with_version}/competing_price", expires_in: 12.hours) do
Competitor::API.find_price(id)
end
end
end
複数レコードに対するキーを生成する
複数レコード(ActiveRecoerd::Relation
)に対するキーの生成メソッドはRailsのバージョンによって仕様が異なるため別々に紹介します。
Rails 6より前の場合: cache_key
Rails 6より前におけるcache_key
は『モデルのクラス名』『レコード数』『updated_atの最大値』の情報を組み合わせたキーを作成するメソッドです。
キーの接頭辞にはquery-
がつき、フォーマットは{table_name}/query-{query-hash}-{count}-{max(updated_at)}
となります。
Rails 6の場合: cache_keyとcache_version
Rails 6からは、Rails 6より前におけるcache_key
がcache_key
とcache_version
に分割されました。4
Rails 6ではcache_key
のフォーマットが{table_name}/query-{query-hash}
、cache_version
のフォーマットが{count}-{max(updated_at)}
になります。
具体的なキーの文字列は以下のようになります。
### 全ユーザーに対するのキー
> users = User.all
> "#{users.cache_key}-#{users.cache_version}"
=> "users/query-7560e7936f3133eab226ece9495d6672-4-20210717061436187421"
### activeなユーザーに対するキー
> users = User.where(active: true)
> "#{users.cache_key}-#{users.cache_version}"
=> "users/query-5c194686f28574fe45d2e91c7fe8e415-3-20210717061436187421"
### レコードが増えるとキー(cache_versionの部分)が新しくなる
> User.create(name: "Yamada", active: true)
> users = User.all
> "#{users.cache_key}-#{users.cache_version}"
=> "users/query-7560e7936f3133eab226ece9495d6672-5-20210717063147946915"
複数のインスタンス(ActiveRecord::Relation)をキャッシュする場合の実装方法
ActiveRecord::Relation
はActiveRecord::FinderMethodsと異なり遅延評価です。
where
などで取得したタイミングではクエリは実行されず、each
などで呼び出されたタイミングで初めてクエリが実行されます。
ですので、SQLの実行回数を減らすためにキャッシュを利用しようと考えた場合、ActiveRecord::Relation
をキャッシュしても効果がないので注意が必要です。
以下では複数のインスタンスをActiveRecord::Relation
を利用せずにキャッシュする方法について紹介します。
to_aなどを利用して即時実行する
即時実行されるto_a
メソッドを足すことでActiveRecord::Relation
によって表現されていたインスタンスをキャッシュできます。
active_users = Rails.cache.fetch('active_users') do
User.where(active: true).to_a
end
主キーのみキャッシュをする
Rails.cache.fetch を正しく運用する!Active Record オブジェクトをキャッシュしたい時の記事ではActiveRecord::Relation
に対してto_a
などを利用してデータをキャッシュする方法のデメリットとして以下を挙げています。
- ActiveRecord::Relationのデータサイズが大きい場合、キャッシュ領域を大量に消費してしまう
- キャッシュされたデータがActiveRecord::Relationではなくなるため、呼び出し元でwhere等のメソッドチェーンが利用できない
上記の解決策として、当該記事では主キーのみをキャッシュするという方法が提案されています。
主キーのみのキャッシュではActiveRecord::Relation
を取得するたびにクエリが実行されますが、主キー検索は高速なのでそこまで心配する必要はありません。
active_user_ids = Rails.cache.fetch('active_user_ids') do
User.where(active: true).pluck(:id)
end
active_users = User.where(id: active_user_ids)
まとめ
- Rails.cacheの主要メソッドはread, write, delete, exist?, fetch
- キャッシュストアの種類はファイルシステム, メモリ, memcached, Redis, そのほか
- キャッシュストア先はconfig.cache_storeで設定する
- キャッシュのキーはRails.cacheのcache_key_with_version, cache_key, cache_versionを活用するとよい
- ActiveRecord::Relationは遅延評価なのでキャッシュする際は注意する
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!