Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

前置き

未だに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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした