zdogma's diary

徒然なるエンジニアな日々。

find_each と find_in_batches の違い

ことはじめ

大量のデータにアクセスして処理を行うとき、全てメモリに展開するとよろしくないので、 少しずつメモリに展開しながら処理をしよう、というのはよくあるケースだと思いますが、 Rails を使っている場合は下記の2メソッドが候補に上がるかと思います。

  • find_each
  • find_in_batches

それぞれの使い分けがいまいちわかっていなかったので、調べてみました。

それぞれのメソッドについて

find_each

デフォルトで1,000件 ( batch_size で指定する)ずつデータを取得し、 取得したデータを 1件ずつ 処理する。

User.find_each(batch_size: 2) { |user| p user }
=> 
User Load (0.3ms)  SELECT  `users`.* FROM `users`  ORDER BY `users`.`id` ASC LIMIT 2
#<User id: 1, ..>
#<User id: 2, ..>
User Load (0.2ms)  SELECT  `users`.* FROM `users` WHERE (`users`.`id` > 2)  ORDER BY `users`.`id` ASC LIMIT 2
#<User id: 3, ..>
..

find_in_batches

デフォルトで1,000件 ( batch_size で指定する)ずつデータを取得し、 取得したデータを 配列 で処理する。

User.find_in_batches(batch_size: 2) { |users| p users }
=>
User Load (0.3ms)  SELECT  `users`.* FROM `users`  ORDER BY `users`.`id` ASC LIMIT 2
[#<User id: 1, ..>,  #<User id: 2,..>]
User Load (0.2ms)  SELECT  `users`.* FROM `users` WHERE (`users`.`id` > 2)  ORDER BY `users`.`id` ASC LIMIT 2
[#<User id: 3, ..>,  #<User id: 4,..>]
..

結局何が違うのか?

「find_each が1件ずつ処理するなら、batch_size を指定する意味ってなんなの?」
など、いろいろ気になるところがあったので、実際のコードを読んでみたところ、謎が解けました。

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_each は find_in_batches で取得した配列に each を噛ませている という実装だったのですね。

find_in_batches(options) do |records|
  records.each { |record| yield record }
end

なんとなく別物だと思い込んでしまっていたので、この背景を知ってスッキリしました。

参考にしたページ

Bundler による BUNDLED WITH に対応する

Bundler とは?

言わずと知れた Gem のパッケージ管理ツールです。

bundler/bundler · GitHub

Gemfile に必要なパッケージについて記載し、 bundle install すれば必要なパッケージをすべてインストールしてくれます。

突然の BUNDLED WITH

たまたま訳あってローカルで gem update していたところ、突然 Gemfile.lock に下記のような内容が追記されるようになりました。

BUNDLED WITH
   1.10.3

どうやら Gemfile.lock の中で bundler 自体のバージョンも管理するようになったようです。

上記自体は bundler の 1.10.0.pre.2 からあったということですが、なかなか bundler 自体を update する機会がなく、今まで目に触れる機会がありませんでした。

今後メンバーが増える予定があったりして、チーム内で bundler のバージョンが 1.10.x を境に混在する可能性があったので、 今日足並みを揃えてローカルとサーバーの bundler をバージョンアップし、上記 Gemfile.lock の変更を commit しました。

上記の過程でいろいろと調べていたところ、Issue上でいろいろと議論がなされていましたが、 ちょうど10日前の commit のメッセージ部分に現状の方針が書いてありました。

  1. When running bundle install, Bundler will update the version in the lockfile if newer than the version present.
  2. Then, check in the lockfile change, exactly as you would after running install to change any other gem version.
  3. Older versions of Bundler will not change the number in the lock, but will warn the user that they are out of date.

github.com

今まで bundler 自体のバージョンについてあまり意識せずに開発してきたので、 ローカルとサーバーでバージョンが違ったり、というケースもままあったのですが、 本来的には使っているパッケージのバージョンは正しく管理すべきなのだから、 bundler 自体のバージョンもちゃんと管理しよう、ということなのでしょうね。

参考にしたページ