Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: ジョブの中で関連付けをループしないこと(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

Rails: ジョブの中で関連付けをループしないこと(翻訳)

SQLを直接使わずにRubyでデータベースとやりとりする方法は、ともすると、ほとんど同じようなクエリをうっかり大量に実行してパフォーマンス低下で頭を抱えがちですが、それでもActive Record(または同等のもの)を利用するメリットには一般にそれだけの価値があります。

これはN+1クエリ問題と呼ばれるのが普通です。ビューでActive Recordオブジェクトを表示し、belong_toでそれに関連付けられているレコードをすべて表示しようとするときに最もよく見かけます。どうしてもデータベースから多数のレコードを読み込んだり保存したりする必要がある場合は、この重たいデータベース処理を非同期ジョブに逃がす方法が使えます。

しかし、production環境のデータがローカル環境よりどれだけ多いかということを軽く考えていると、ジョブの「内部で」ループが長時間実行されてしまうということになりがちです。

以下のように書くのではなく

ジョブの中でも多数のActive Recordオブジェクトをループで回して更新する。

class UpdateManyCommentsJob < ApplicationJob
  def perform(post)
    post.comments.each do |comment|
      comment.update(name: comment.name.downcase)
    end
  end
end

以下のように書くこと

更新ごとにジョブを分ける。

class UpdateManyCommentsJob < ApplicationJob
  def perform(post)
    post.comments.each do |comment|
      UpdateSingleCommentJob.perform_later(comment)
    end
  end
end

class UpdateSingleCommentJob < ApplicationJob
  def perform(comment)
    comment.update(name: comment.name.downcase)
  end
end

そうする理由

ここでやっているのは、作業をできるだけ並行化して個別の作業要素を小さく保つことです。

sidekiqの作者Mike Perhamはジョブについての知見が深く、sidekiqのベストプラクティスで「ジョブは、多数のジョブを並行実行できるように設計せよ」と述べています。提案されているソリューションでは、1個のジョブを長時間回すのではなく、1個の親ジョブが多数のサブジョブをキューに登録するようにしています。これにより多くのメリットが得られます。

バックグラウンドのキューで多数のワーカーを実行する形にすれば、ループで処理を1個ずつ回すのに比べて更新に要する時間が大幅に短縮されます。

1件の投稿(post)に「大量の」コメントが付いているような場合でも、リトライが難しくメモリ肥大化問題を引き起こす可能性のある1個のジョブを長時間動かすことを回避できます。

ジョブ内で回すループはproduction環境でバグの温床になる可能性があります。どうかご用心ください。

そうしない理由があるとすれば

1個のジョブでhas_many関連付けを小さなループで更新するのは問題ありませんが、production環境ではそうはいきません。

ジョブの内容によってはRailsの他の機能が使えるかもしれません。update_allで関連付けをSQLクエリ1件でまとめて更新する手もありますが、update_allによる更新はシンプルにしなければならず、関連付けられたレコードでコールバックが実行されない点に注意が必要です。

関連記事

Rails: Active Record関連付けのループではeachよりfind_eachを検討しよう(翻訳)

Rails: SidekiqはActive Jobを経由せずに直接使おう(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。