読者です 読者をやめる 読者になる 読者になる

hotoolong's blog

Railsやvimや気になったことを綴ってます

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

ruby rails

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っぽく書けるのでいいですね。