LoginSignup
51
40

More than 3 years have passed since last update.

【Rails 4.2】find_each と find_in_batches の違いと使い分け&【Rails 5.0~】in_batches

Last updated at Posted at 2018-10-28

前置き

未だにRails 4.2を使っている方向けです。
最後にRails 5.0以降についても補足しております。

出発点

数十万いる全ユーザーに対し、カラムを新規追加処理を、1Gしかないサーバーで実行したらメモリ不足で落ちました。
初めに書いていたのは以下のようなコードです。

User.all.each do |user|
  user.update_new_column
end

数十万人のユーザーがメモリにドーンとくるので、そりゃ落ちます。

find_eachとfind_in_batchesとの出会い

先輩に「find_eachってのがあるから使ってみな」というアドバイスを頂き、調べてみました。
どうやら少しずつデータを取ってこれるようです。
defaultは1000レコードです。
すると似たようなfind_in_batchesもHitしました。
Rails ドキュメントを読んだのですが、違いはすぐにはわかりません。
http://railsdoc.com/references/find_each
http://railsdoc.com/references/find_in_batches

本題の挙動の違いと使い分け

「出どころの怪しいブログ記事ではなく、一次情報に当たれ!」と日頃よく言われているのでみてみます。

find_each

    def find_each(options = {})
      if block_given?
        find_in_batches(options) do |records|
          records.each { |record| yield record }
        end
      else
        enum_for :find_each, options do
          options[:start] ? where(table[primary_key].gteq(options[:start])).size : size
        end
      end
    end

find_in_batches

 def find_in_batches(options = {})
      options.assert_valid_keys(:start, :batch_size)

      relation = self
      start = options[:start]
      batch_size = options[:batch_size] || 1000

      unless block_given?
        return to_enum(:find_in_batches, options) do
          total = start ? where(table[primary_key].gteq(start)).size : size
          (total - 1).div(batch_size) + 1
        end
      end

なんとfind_eachの中でfind_in_batchesが呼ばれておる・・・。
違いは、find_in_batchesはまとめて取ってきたデータを一旦配列に入れるのに対し、
find_eachはfind_in_batchesで取ってきた配列をeachでかけているということでした。

なので使い分けとしては、
find_in_batches: 取ってきた1000件に対し、何か処理をしてからeachにかけたい時。(super coolな用途例が思い浮かばない。。)
find_each: ただただ全てを単純にeachにかけたいとき
だと思います。
今回で言えば、find_eachでよし。

補足 (Rails5以降)

Rails5以降ではin_batchesというメソッドがあるらしいです。
これはfind_in_batchesがArrayで返すのに対し、ActiveRecord::Relation objectsで返すようです。
だからこんなこともできちゃう。(以下ここから抜粋)

People.in_batches(of: 100) do |people|
  people.where('id % 2 = 0').update_all(sleep: true)
  people.where('id % 2 = 1').each(&:party_all_night!)
end

find_in_batchesはすぐには良い用途が思い浮かばなかったですが、こっちは色々と使えそうですね。

これにより、find_eachは中でfind_in_batchesを呼んでいて、

    def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
      if block_given?
        find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do |records|
          records.each { |record| yield record }
        end
      else
        enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do
          relation = self
          apply_limits(relation, start, finish).size
        end
      end
    end

find_in_batchesは、中でin_batchesを呼んでいるという構造に。

    def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
      relation = self
      unless block_given?
        return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do
          total = apply_limits(relation, start, finish).size
          (total - 1).div(batch_size) + 1
        end
      end

      in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore) do |batch|
        yield batch.to_a
      end
    end

美しい〜

参考

http://zdogma.hatenablog.com/entry/2015/06/16/221617
https://qiita.com/nikadon/items/9e6431f5a7b2b8798113
https://github.com/rails/rails/pull/20933
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/batches.rb
https://raw.githubusercontent.com/rails/rails/4-2-stable/activerecord/lib/active_record/relation/batches.rb

51
40
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
40