モンモンブログ

技術的な話など

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 で囲みました。これでセーフ。

気をつけましょう。