前置き
未だに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