0
0

Railsで、グルーピングしたDBレコードを1クエリで取得するscope

Last updated at Posted at 2024-07-26

動機

特定カラムが同じレコードを、そのどれでもいい一つのみを代表して重複を一つにまとめ、レコードと対応するインスタンスとして取得する...というクエリを組むとき苦労したので備忘録です。
当初distinctメソッドを利用してみようとしていましたが、primary_keyが取得できず(selectメソッドにidを組み込むと、idが異なることで重複していないとみなされるため)、レコードと対応するインスタンス化できませんでした。
特定カラムのみをselectしてdistinctすると、idがnil、カラムがまとめた値、という形になります。

distinctの参考:

上記記事のdistinctはselectしなくても問題ないか?のような操作がやりたかったイメージです。

グルーピングという解決策

以下の記事を参考に、解決に一歩近づくことができました。
parent_hoge_idカラムでグルーピングして、グループごとに最大のIDの1レコードを取得しています。

HogeModel.where(id: { HogeModel.group(:parent_hoge_id).select('max(id)') })

scope化

これを毎回書くのはわかりにくいし面倒なのでscope化しようと思いました。
まず以下のようにしました。

scope :uniq_by_parent_hoge_id, -> { where(id: HogeClass.group(:parent_hoge_id).select('max(id)')) }

しかし後日気付いたのですが、当然これを利用するシーンでは以下のように他の条件を重ねることになります。

HogeModel.where(ex_condition).uniq_by_parent_hoge_id

この場合、グルーピングのためのHogeClass.group(:parent_hoge_id).select('max(id)')での絞り込みは、前者のex_conditionを満たすとは限りません。
where条件で絞り込んだものをグルーピングしたいので、以下のようなチェーンにしたいのですが、上のscopeではそのようにはできません。

HogeModel.where(id: { HogeModel.where(ex_condition).group(:parent_hoge_id).select('max(id)') })

ということで、修正したscopeが以下のようになりました。

scope :uniq_by_parent_hoge_id, -> { HogeModel.where(id: group(:parent_hoge_id).select('max(id)')) }

groupメソッドは、selfから呼ばれています。
scope内でのselfは、このscopeメソッドを呼んだ、クラスやwhereチェーンなどになっています。つまり今回の例ではHogeModel.where(ex_condition)です。
これにより、目標のクエリの発行が可能になりました。

まとめと注意

SQLクエリから考える、いい勉強になりました。
注意点としては、このscopeを呼んだ際は呼び出し元のクエリに基づいてグルーピングを行うので、やりたい絞り込みをチェーンでつないだ最後に呼ぶ必要があるということになります。
以上、何かの役に立てば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0