はじめに
Railsのプロジェクトでキャッシュを使う際のパターンをちょっと考えました。
一般的なActive Recordのキャッシュというよりは、複数のモデルに紐づく返り値のキャッシュをどうするかという観点で、もしもっと良い方法とか一般的なアーキテクチャパターンあれば教えてください。
前提条件
- 複数モデルに紐づくAPIの返り値をキャッシュしたい
- 複数モデルに紐づくが、その中の一つでもupdateされたらキャッシュを削除したい
アンチパターン
アンチパターンという言葉はちょっと強いですが、パッと思いつく方法は以下のように紐づくモデルにafter_commit
でキャッシュを削除するコードを追加する方法です。
例えば、以下のような場合です。
class Api::MyApiController < ApplicationController
def index
cache_key = "my_api_cache_key"
result = Rails.cache.fetch(cache_key, expires_in: 12.hours) do
# 複数のモデルからデータを取得する処理
model1_data = Model1.all
model2_data = Model2.all
# 必要なデータのマージ処理
{ model1: model1_data, model2: model2_data }
end
render json: result
end
end
class CacheClearService
def self.clear_my_api_cache
Rails.cache.delete("my_api_cache_key")
end
end
# 各モデルのコールバックでサービスクラスを利用
class Model1 < ApplicationRecord
after_commit :clear_cache, on: [:update]
private
def clear_cache
CacheClearService.clear_my_api_cache
end
end
class Model2 < ApplicationRecord
after_commit :clear_cache, on: [:update]
private
def clear_cache
CacheClearService.clear_my_api_cache
end
end
これでももちろん良いのですが、この場合の問題点としてはキャッシュを使ってる側では、どのタイミングでキャッシュが削除されているか分からないところです。
使われているモデルを全て見に行かないと、いつキャッシュが削除されているかわかりません。
解決策
キャッシュの呼び出し側で紐づくモデルが分かれば、少なくともキャッシュ削除に関連するモデルは分かります。
そこで、ActiveSupport::Notifications
を使います。
class CacheClearService
CACHE_KEY = 'my_api_cache_key'
MODEL1_EVENT_KEY = 'clear_my_api_cache_model1_event'
MODEL2_EVENT_KEY = 'clear_my_api_cache_model2_event'
def self.setup_cache_cleanup_listener
ActiveSupport::Notifications.subscribe(MODEL1_EVENT_KEY) do
self.clear_cache
end
ActiveSupport::Notifications.subscribe(MODEL2_EVENT_KEY) do
self.clear_cache
end
end
def self.clear_cache
Rails.cache.delete(CACHE_KEY)
end
end
class Model1 < ApplicationRecord
after_commit :clear_cache, on: [:update]
private
def clear_cache
ActiveSupport::Notifications.instrument(CacheClearService.MODEL1_EVENT_KEY, self)
end
end
class Model2 < ApplicationRecord
after_commit :clear_cache, on: [:update]
private
def clear_cache
ActiveSupport::Notifications.instrument(CacheClearService.MODEL2_EVENT_KEY, self)
end
end
# initilizerでリスナーを初期化
Rails.application.config.to_prepare do
CacheClearService.setup_cache_cleanup_listener
end
このようにActiveSupport::Notifications
を使ってイベントをsubscribeする形にすることで、少なくともCacheClearServiceを見れば、どのモデルが関係ありそうとか、イベントを一覧で確認することができますので便利かなと思います。
まとめ
お互いに疎結合のためこれを導入してもテストが落ちることがないというのはある意味で利点ですが、モデル側のキャッシュをクリアするイベントが削除されても、CacheClearService側からは分からないところは欠点かと思いますので、メリットデメリット把握した上で導入されるのが良いと思います。