Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails tips: Active Recordの`#from`を使ってorderとdistinctを1つのクエリにする(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: Active Recordの#fromを使ってorderとdistinctを1つのクエリにする(翻訳)

重複のない一意の結果を得ようとしたことがある方は、単純なクエリでこれを実現できないことにおそらくお気づきかと思います。Active Recordでは、操作する各カラムを明示的にselectすることが要求されるためです。わかりやすくするため、次の事例を考えてみましょう。

問題

Location.joins(:users).where(users: {enabled: true}).distinct.order('locations.name')

目的は、enabled: trueuserlocationごとに1件以上表示することです。locationのリストをnameでソートする必要もあります。上のクエリは動作しません。実行すると以下のエラーが発生します。

ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'locations.name'

解決方法

基本的には2つのクエリが必要です。

  1. enabled: trueuserを1件以上持つlocationをフェッチするクエリ
  2. locationnameで並べ替えるクエリ

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つ取っています。

  1. FROMで使われるクエリ
  2. 後で操作できるようにするためのサブクエリとして使われる名前

生成される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をご自由にダウンロードいただけます。

関連記事

Rails tips: モデルのクエリをカプセル化する2つの方法(翻訳)

Rails tips: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)


CONTACT

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