ActiveRecordで子の数を揮発性キャッシュに保存する

ActiveRecordで1:Nの関係を扱うとき、ある親レコードに対して子レコードが幾つ存在するかという情報を、都度計算するのではなくキャッシュしておきたいケースは多いと思います。代表的な実装方法として、ActiveRecordのcounter cacheの機能を利用し、親レコード内にキャッシュを保存しておくという方法があります。

今回は、この情報を親レコード内ではなく揮発性のキャッシュに保存させるために、volatile_counter_cacheというGemをつくりました。memcachedやredisなどのKVSにキャッシュを保存することを想定しています。

方針

以下のような方針に基づいたキャッシュ戦略を想定しています。

  • RDB上には正規化された一次データだけ保存する
  • 非正規化された二次データはRedis等のKVSに保存する
  • 二次データは揮発しても良いようにする

概観

端的に言うと、volatile_counter_cacheは以下のように動作します。

  1. 初めて必要になったときにキャッシュから取得する
  2. キャッシュに無ければRDBから取得してキャッシュに入れる
  3. 一次データが変更されるときは関係するキャッシュを消す

volatile_counter_cache

次のコードを例にして考えましょう。 tweetごとに幾つかのfavoritesを付けられるという関係になっています。

class Tweet < ActiveRecord::Base
  has_many :favorites
  volatile_counter_cache :favorites, cache: Rails.cache
end

counter method

volatile_counter_cache :favorites を実行すると、Tweet#favorites_countというメソッドが定義されます。このメソッドは、要点で述べた「初めて必要になったときにキャッシュから取得する」「キャッシュに無ければRDBから取得してキャッシュに入れる」という処理を行うものです。具体的には、volatile-counter-cache/Tweet/:idというキャッシュキーでRails.cacheに問い合わせ、値が無ければfavorites.sizeを実行して結果をキャッシュに保存します。

callback

また volatile_counter_cache :favorites により、Favorite.after_createFavorite.after_destroy で関連するtweetのキャッシュを削除するコールバックも登録されます。要点の3で述べた「一次データが変更されるときは関係するキャッシュを消す」という処理にあたります。

おわり :bow:

テーブルにカウンター用のカラムを用意したくないときの選択肢としてお使いください。どういうケースでカウンター用のカラムを用意すべきか、カウンター用のテーブルを用意すべきか、KVS等に保存すべきかという判断に迷いがあったので、とりあえず実装コストを考慮から外すべく実装を用意してみた次第です。何か分かりやすい良い指針があれば教えてください :pray: