in_batches では order できない!?

はじめまして!サーバサイドエンジニアと見せかけてフロントエンドエンジニアと思いきや、やっぱりサーバサイドエンジニアの hikaru-kimi です!

技術ブログ初投稿です!どうぞお見知りおきください!

今回のテーマは、Ruby on Rails の ActiveRecord::Batches の in_batches についてです!

Re:lation 開発では大量のデータを扱うことが多いのですが、大量のデータに対して each 処理を行うとメモリを大量に消費してしまい、パフォーマンスの悪化に繋がります。 そんなときに in_batches を用いて(デフォルト値で言えば)1000件ずつ処理することで、少しでもメモリ消費を抑えたいです。

しかし!この in_batches にはとんでもない罠があります! なんと、in_batches を用いる際は返される ActiveRecord::Relation のソート方法を指定することができないのです!!

従って、ループ処理の実行順に意味があるような処理を行う場合は要注意です! (ちなみに、find_in_batches や find_each も同様です)

では実際に Rails のソースコードを読んでみましょう。

in_batches の定義は以下の通りです。

https://github.com/rails/rails/blob/v7.0.4.3/activerecord/lib/active_record/relation/batches.rb#L204

そして、内部では reorder(batch_order(order)) されてます。

relation = relation.reorder(batch_order(order)).limit(batch_limit)

https://github.com/rails/rails/blob/v7.0.4.3/activerecord/lib/active_record/relation/batches.rb#L224

この batch_order の定義を読むと、なんと primary_key でソートされてますね! ( Rails アプリでは基本的には id になりますね)

def batch_order(order)
  table[primary_key].public_send(order)
end

https://github.com/rails/rails/blob/v7.0.4.3/activerecord/lib/active_record/relation/batches.rb#L282-L284

実際に発行される SQL も確認してみましょう! 以下はサンプルアプリの rails c にて comments テーブルのレコードを created_at でソートして、かつ  in_batches でまとめて取得した例です。

irb(main):001:0> Comment.order(:created_at).in_batches{ |comment| p comment.inspect }
Scoped order is ignored, it's forced to be batch order.
   (0.2ms)  SELECT  "comments"."id" FROM "comments" ORDER BY "comments"."id" ASC LIMIT ?  [["LIMIT", 1000]]

勝手に comments テーブルの主キーである id の昇順でソートされてしまっていますね! しかも、ご丁寧に指定した並べ替えは無視される旨出力されてます!

確かにフレームワークは便利です。しかし時には実装者の意図せぬ挙動をする場合もあります。 そのような場合は、丁寧にSQLを解読し、冷静にフレームワークのソースコードを追うことで問題解決に努めたいですね!

弊社インゲージでは、フレームワークをただ利用するだけでなく、そのソースコードをも追えるような開発意欲の高いエンジニアを募集しております!

社内でも、積極的かつ精力的にOSSへのコントリビュート活動を行うエンジニアが在籍しております!

最後までお読みくださったあなた!是非以下のリンクより、まずは気軽にカジュアル面談からのご応募をよろしくお願いいたします!