Edited at

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

More than 1 year has passed since last update.


問題提起

通常、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が無難かなとは考えています。

他にもいい方法があるようであれば、教えてもらいたいです。