カテゴリー:
Rails
タグ:
 Rails after_initialize after_find initialize find where size length count

このエントリーをはてなブックマークに追加
更新日時:
2014年03月05日(水)
作成日時:
2013年06月08日(土)

前の記事 / 次の記事

after_initialize, after_findフィルター はfind系メソッドの仕様上要注意なので、
find系メソッドは捨ててwhereメソッドを使おうという話。

あとコレクションの大きさを調べる時も注意。

目次

  1. 何が危険か
  2. 危険な例
  3. 危険を回避するには
  4. コレクションの大きさを調べる時も危ない
  5. 参考ページ

1.何が危険か

after_initialize
オブジェクトがインスタンス化された時に実行される
after_find
オブジェクトが検索されて発見された時に実行される

で、この時のfind系メソッドの仕様が要注意で、
Railsのfind系メソッドは、そのオブジェクトが実際に使われるかどうかに関係なく、
検索してヒットした全てのオブジェクトに対して after_find をキックし、
検索してヒットした全てのオブジェクトをインスタンス化している(=> after_initialize をキックする)。

例えば、Model.all した時は全件数分の after_find と afterinitialize がキックされる。

つまり after_initialize や after_find に重い処理を書くと、
Model.all 等で大きなコレクションを生成したタイミングで簡単に爆死出来る。

先に結論を書くと、これを回避するにはfind系メソッドはさっさと捨ててwhereメソッドを使う
または、場合によっては initializeメソッドのオーバーライドで代替出来る。

※ [2014/03/05 追記]
Rails4からはallの挙動が変わっているのでRails4においてはModel.allしても爆死しない。

具体的にはRails3→Rails4で次のような変更が入っている。

Rails3: Model.all は オブジェクトのArrayを返す(全てのオブジェクトを生成して配列にする)。
Rails4: ActiveRecord::Relationを返す(評価されるまでオブジェクトは生成されない)。

Rails4においてもModel.all.each { |model| model.id } 等とした場合は同様に爆死し得るが、
この場合はそもそも実際にそのオブジェクトが必要ということなので、
これで爆死する場合はallの仕様に関係なくそもそも重いコードだということである。

参考

2.危険な例

次のような意味不明なモデルとメソッドがあったとして、

def Danger < Activerecord::Base
  after_initialize :danger_method

  def danger_method
    10000.times { |outer|
      10000.times { |inner|
        "nothing to do"
      }
    }
  end
end

次のようにすると、

Danger.all

もし Danger.all.count が100だったら100回、1000だったら1000回 danger_method が走って爆死する。

3.危険を回避するには

  1. find系メソッドを捨ててwhereメソッドを使う
  2. initializeメソッドのオーバーライドで代替する
  3. after_initialize と after_find は使わない

3.1.find系メソッドを捨ててwhereメソッドを使う

そもそもfind系のメソッドは Rails4からは廃止 される時代遅れなものなのでwhereメソッドに切り替える。
whereメソッドを使った場合、そのオブジェクトが実際に必要になるまで after_initialize, after_find は走らない。

例えば次のように書き換える。

Model.all
=> Model.where(nil)
# Rails4からは Model.all でオブジェクトの配列ではなくてリレーションを
# 返すようになっているので、思わずオブジェクトが大量に生成される危険はない模様。

Model.find(:all, :conditions => ["name like ?", "K%"])
=> Model.where("name like ?", "%K")

findをどうwhereに書き換えればいいかはこの辺のページを参照。

3.2.initializeメソッドのオーバーライドで代替する

どうしてもfind系メソッドを捨てたくないか捨てられない場合、
場合によっては initializeメソッドのオーバーライドで代替出来る。

場合によってはというのは new するタイミングでしか走らなくても問題が無ければ。

def Danger < Activerecord::Base

  def initialize(attributes = {}, options = {}
    super

    10000.times { |outer|
      10000.times { |inner|
        "nothing to do"
      }
    }
  end
end

と書いた場合、

Danger.new                   => キックされる
Danger.where(nil).first      => キックされない
Danger.where(:id => 1).first => キックされない

super して祖先の initialize を呼ばないと 元々のinitializeメソッドを破壊してしまうので注意。

3.3.after_initialize と after_find は使わない

使わない。使わなければ何も起こらない。
頑張って他のところで処理する。

4.コレクションの大きさを調べる時も危ない

Railsでコレクションの大きさを調べるのには size, count, length がある。
で、これらは全部同じ結果を返すんだけど結果を返すまでの過程が違って、
lengthを使った時は after_initialize と after_find が走る。

それぞれの違いは次の通り

10.times { Book.create!({:author => "Ore"}) }
books = Book.where(:author => "Ore")

books.size
# => 10
# select count(*) を発行して得た件数を返す。
# 既に発行されていた場合はキャッシュから件数を得る。

books.count
# => 10
# select count(*) を発行して得た件数を返す。
# 常に select count(*) を発行して件数を得る。

books.length
# => 10
# 各オブジェクトのインスタンスを生成し、生成されたオブジェクトの件数を返す。
# 既にロードされていた場合はロード済みのコレクションから件数を得る。
# つまり Books.all.size とほぼ同じ。
# よって after_initialize と after_find がキックされる。

参考

5. 参考ページ