Note

3年後の自分のために書いています

ActiveRecord の `#all`, `#find_each`, `#find_in_batches`, `#in_batches` の違いについて

表題の件、ずっと曖昧に理解していた。

1.2 複数のオブジェクトをバッチで取り出す - Active Record クエリインターフェイス - Rails ガイド

Rails のコードも読みながら ↑ の内容にプラスα してまとめる。

結論

処理するレコードが 1000 件以下なら何使っても一緒。

1000 件以上になってくると、 #all ではなく、 #find_each, #find_in_batches, #in_batches など ActiveRecord::Batches のメソッドを用途に合わせて使い分けていくのが良い。

#all

Returns an ActiveRecord::Relation scope object.

該当コードは ここActiveRecord::Relation オブジェクトを返す。

users = User.all
users.size # Fires "select count(*) from  users" and returns the count
users.each {|user| puts user.name } # Fires "select * from users" and loads user objects

テーブル全体を一度に取り出し、レコード 1 行ごとにオブジェクトを生成し、そのオブジェクトの配列をメモリに載せるので、メモリを圧迫する可能性がある。

#find_each, #find_in_batches, #in_batches

大事なのはここから。

#find_each, #find_in_batches, #in_batchesActiveRecord::Batches として定義されており、バッチサイズで指定したレコード数ごとに処理できるため、大量のレコードを扱う際にメモリ消費量を削減できる。デフォルトのバッチサイズが 1000 件なので、それよりレコード数が少ない場合は #all を使っても変わらない。

実は上記メソッドの関係は #find_each の中で #find_in_batches を呼び出し、 #find_in_batches の中で #in_batches を呼び出す、という関係になっている。

柔軟性でいうと #in_batches > #find_in_batches > #find_each というわけだ。

#in_batches から見ていく。

#in_batches

Yields ActiveRecord::Relation objects to work with a batch of records.

該当コードは ここ。長いので読めてない。 ActiveRecord::Relation オブジェクトをブロックに渡すので、一番柔軟な使い方ができる。

User.where("age > 20").in_batches do |relation|
  # Relation オブジェクトに対する処理が書ける
  relation.destroy_all
end

relation.where でつなげて絞り込みしたりとか relation.pluck 的な形でも使える。

#find_in_batches

Yields each batch of records that was found by the find options as an array.

該当コードは ここ#in_batches で取り出したバッチを #to_a してブロックに渡す。

User.where("age > 20").find_in_batches do |users|
  # バッチ前処理が書ける
  # 配列に対する処理が書ける(Relation オブジェクトに対する処理は書けない)
  users.each { |user| user.drink_alcohol! }
  # バッチ後処理も書ける
end

#find_each

The find_each method uses find_in_batches with a batch size of 1000 (or as specified by the :batch_size option).

該当コードは ここ#find_in_batches で取得したモデルの配列を #each してブロックに渡す。

User.where("age > 20").find_each do |user|
  # バッチ処理の前後に処理がないなら一番スッキリ書ける
  user.drink_alcohol!
end