ActiveRecordのscope,validatorでの意図せぬキャッシュに要注意
クリティカルなバグの原因になりえます(なりました)。
環境
少々古いです。activerecord の最近のバージョンでは未確認です。
ruby 1.9.3p547 (2014-05-14 revision 45962) [x86_64-darwin13.3.0] rails-3.2.11 activerecord-3.2.11 MacOSX Mavericks
現象
ActieRecord の named scope の条件文や、 validator のエラーメッセージなどは自動でキャッシュされちゃいます。そのため、
scope :upcoming, where('start_at > ?', Time.now)
のように scope の where 句で時刻比較したり、
validates :start_at, presence: { message: I18n.t(:start_at_is_required) }
のように validation のエラーメッセージを I18n で国際化したりすると、1回目に実行した時点での時刻やロケール文字列がキャッシュされてしまい、
- 時間が経過しても古い時刻での検索結果が返されたり
- 日本語ロケールを指定してるのに英語でメッセージが出力されたり
しちゃいます。怖いですね。
対策
Proc で囲むことで回避できます。lambda でも可。
named scope ならこう。
# クエリ全体を Proc.new で囲む scope :upcoming, Proc.new { where('start_at > ?', Time.now) }
validator ならこう。
# I18n 呼んでる部分を Proc.new で囲む validates :start_at, presence: { message: Proc.new{ I18n.t(:start_at_is_required) } } end
named scope から別の named scope を使ってる場合は更に注意が必要です。
時刻比較等の動的な操作を行う named scope (scope A) を、別の named scope (scope B) から使う場合、scope A のみならず scope B も Proc.new で囲む必要があるんです。
# (scope A) 時刻比較する "upcoming" scope。Proc.new で囲んで対策済み scope :upcoming, Proc.new { where('start_at > ?', Time.now) } # (scope B) "upcoming" scope を利用。Proc.new で囲んでない scope :available, upcoming.where(status: 1)
これはアウト。"available" scope の評価結果がキャッシュされ、時刻が変化しません。
# (scope A) こちらを Proc.new で囲む必要があるのはもちろん、 scope :upcoming, Proc.new { where('start_at > ?', Time.now) } # (scope B) "upcoming" scope を利用するこちらも Proc.new で囲まないといけない scope :available, Proc.new { upcoming.where(state: :open) }
両方 Proc.new で囲みました。これでセーフ。
気をつけましょう。