Rails
sort
ActiveRecord
hasMany
n+1問題

has_manyで定義されたアソシエーションを順序を指定して効率的にとってきたい

問題提起

通常、Railsを使って、N+1問題を解決しつつ、子要素以降にorderを指定しようとすると

Parent.eager_load(:children).order(:name, "children.name")

のように書く形になることが多いと思う。

今回はこの問題に関して本当にこれが正しいのか?を考えてみたい

例えば、下記のようなコードで利用することを考える

parents = Parent.eager_load(:children).order(:name, "children.name")
parents.each do | parent |
  # ここでparentのオブジェクトに対する処理を行う
  p parent
  parents.children.each do | child |
    # ここでchildのオブジェクトに対する処理を行う
    p child
  end
end

上記のような利用シーンでは、ParentにChildのデータがJOINされている意味はなく、
parent.childrenで、希望通りの並び順でchildが取得できていればいいだけ。

変更案1

  • children取得後に並べ替える
    • orderから"children.name"を削除
    • joinする必要がなくなるので、eager_loadからpreloadに変更
parents = Parent.preload(:children).order(:name)
parents.each do | parent |
  # ここでparentのオブジェクトに対する処理を行う
  p parent
  parents.children.sort{ |a, b| a.name <=> b.name }.each do | child |
    # ここでchildのオブジェクトに対する処理を行う
    p child
  end
end

ここで、気をつけてほしいのは、parent.children.order(:name)としてしまうと、せっかくpreloadしたのものが使われず、もう1度データ取得されてしまうこと。
※sortメソッドでやるのも無理やり感ありますが。

変更案2

  • 最初からsortしたアソシエーションを用意してそれを利用する
    • joinは不要になるので、eager_loadをpreloadにする

Parentクラスに、下記のようなアソシエーションを追加する(既存のものを変えてもいい場合は変更したほうが簡単です)

has_many :sorted_children, class_name: "Child", ->{ order(:name) }

呼び出し部分を変える

parents = Parent.preload(:sorted_children).order(:name)
parents.each do | parent |
  # ここでparentのオブジェクトに対する処理を行う
  p parent
  parents.sorted_children.each do | child |
    # ここでchildのオブジェクトに対する処理を行う
    p child
  end
end

JOINの何が悪いのか

データ量が増えれば増えるほど、JOINのコスト、sortのコストは増加していきます。
パフォーマンスを考えた場合、不要なJOINをしないことが一番のパフォーマンスチューニングであると考えているので、なるべくJOINを使わない方法を探しました。

まとめ

個人的には変更案2が無難かなとは考えています。
他にもいい方法があるようであれば、教えてもらいたいです。