概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Ruby on Rails / ActiveRecord - order with distinct in one query
- 原文公開日: 2018/01/17
- 著者: Paweł Dąbrowsk
Rails tips: Active Recordの#from
を使ってorderとdistinctを1つのクエリにする(翻訳)
重複のない一意の結果を得ようとしたことがある方は、単純なクエリでこれを実現できないことにおそらくお気づきかと思います。Active Recordでは、操作する各カラムを明示的にselectすることが要求されるためです。わかりやすくするため、次の事例を考えてみましょう。
問題
Location.joins(:users).where(users: {enabled: true}).distinct.order('locations.name')
目的は、enabled: true
のuser
をlocation
ごとに1件以上表示することです。locationのリストをname
でソートする必要もあります。上のクエリは動作しません。実行すると以下のエラーが発生します。
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'locations.name'
解決方法
基本的には2つのクエリが必要です。
enabled: true
のuser
を1件以上持つlocation
をフェッチするクエリlocation
をname
で並べ替えるクエリ
Active Recordのfrom
メソッドを使えば、これを1つのクエリにできます。そのためには、Location
モデルを少々リファクタリングしなくてはなりません。クエリロジックを分離するためにクラスメソッドを2つ追加します。変更後のモデルは次のようになります。
class Location < ActiveRecord::Base
has_many :users
def self.enabled
joins(:user).where(users: {enabled: true}).distinct
end
def self.by_name
order(:name)
end
end
上のリファクタリングについて詳しく知りたい方は、過去記事をご覧下さい。
この構成にすることで、from
メソッドを使えるようになります。
Location.from(Location.enabled, :locations).by_name
このfrom
メソッドをもう少し詳しく見てみましょう。このメソッドは引数を2つ取っています。
FROM
で使われるクエリ- 後で操作できるようにするためのサブクエリとして使われる名前
生成されるSQLクエリは次のようになります。
SELECT `locations`.* FROM(SELECT DISTINCT `locations`.* FROM `locations` INNER JOIN `users` ON `users`.`location_id` = `locations`.`id` WHERE `users`.`enabled` = 1) ORDER BY `locations`.`name`
お知らせ: コードを正しくテストするには
コードを正しくテストするのは何かと困難であり、しかも最も大変なのはテストを書き始めるときです。テストを書き始めるときに役立つRSpec & Test Driven Developmentの無料ebookをご自由にダウンロードいただけます。