hotoolong's blog

プログラムのことやエンジニアリングに関することを記事にしています。

ActiveRecord::Base#find_in_batchesを使ってみた

Railsバッチ処理したい時に既存の
ActiveRecord::Base#findを使いたいけど、処理件数が多くなりすぎるとメモリ食い過ぎて大変。
なんてことになりそうな場合は、今まではActiveRecord::Base#connectionで直接SQL文を実行してました。

ActiveRecord::Base#connection

ActiveRecord::Base.connection.execute("select id, name from users where status=1")

ってな感じですね。
executeを使うことでレコード単位でDBに問い合わせしてくれるので、メモリへの負荷が軽減します。
これで幸せって思ってたのですが、executeを使うとMysql::Resultで返却されます。
Arrayの中にArrayになっているので取り出すときに面倒なのです。
しかも、中身はすべてStringです。

[["1","aa"],["2","bb"]]

みたいな感じで返ってきます。
なので、

param = {:id => 0, :name => 1}
result.each do |row|
  p "#{row[param[:id]]} #{row[param[:name]]}"
end

という感じで取り出さないといけないのです。。
しかも、その後処理するときに面倒ですね。。
(見た目はそれほど面倒じゃなさそうですが、、)

ActiveRecord::Base#find_in_batches

そこで、find_in_batchesを使うとRailsのコードを利用しつつメモリへの展開するオブジェクトも削減できます。

UPDATE_SIZE=100
User.find_in_batches(:conditions => ["status=1"], :batch_size => UPDATE_SIZE) do |users|
  users.each do |user|
    p "#{user.id} #{user.name}"
    #条件に一致するデータのフラグをなどの処理をいれる。
  end
end

こうすることで
UPDATE_SIZE分だけのレコードを取得してくれます。
中身は何をやってくれているのかというと、

SELECT * FROM `users` WHERE (users.id > 282199) AND (status = 1) ORDER BY users.id ASC LIMIT 100

id順にならべて取得できたidを覚えていて次の実行時にそれ以上のidを取得するということをやってます。
これならlimitでページ送りのような処理になっていないので、パフォーマンスも速いです。
なので、なかなか賢い感じで、Railsっぽく書けるのでいいですね。