Rails.cacheの使い方、低レベルキャッシュの利用方法

Ruby

特定の値やクエリ結果をキャッシュすることを低レベルキャッシュと呼びます。
今回は低レベルキャッシュを実装する際に利用されるRails.cacheについて紹介します。1

Rails.cacheについて

RailsではActiveSupport::Cache::Storeを利用することでデータのキャッシュができます。
Rails.cacheとはRailsアプリケーションからActiveSupport::Cache::Storeを操作するメソッドです。

Rails.cacheによるデータ操作に関するメソッドについて

Rails.cacheの主要メソッドは以下の通りです。2

Rails.cacheの主要メソッド
  • 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_keycache_keycache_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::RelationActiveRecord::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まとめ
  • 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)やってます。フォローしてもらえるとうれしいです!

参考記事