QA@IT

Activerecordのincludesで、結合条件を書くことはできますか?

6575 PV

SQL では以下のように結合条件を書くことができますが、これを Activerecord の includes で実現する方法はあるでしょうか?
entries.class_id = 10 は WHERE句に書くと結果が変わってしまうので、結合条件としてしか書くことができません。

SELECT * FROM students 
LEFT OUTER JOIN entries on entries.student_id = student.id AND entries.class_id = 10
LEFT OUTER JOIN classes on entries.class_id = classes.id

student は生徒、class は授業で、entry は履修を表します。
student と entry、class と entry はいずれも一対多の関係で、ひとりの student はひとつの class に entry を0か1持っています。

授業の詳細画面で生徒の検索機能を実装し、検索結果の一覧を表示しようとしています。
表示する生徒情報の中にその授業を履修しているかどうかを加えたいのですが、以下のようなコードをビューに書くとN+1 問題が発生する上、正常な情報を表示できません。。

#class.rb
students = Studens.joins("LEFT OUTER JOIN entries ON entries.student_id = users.id AND entries.class_id = #{class.id.to_i} LEFT OUTER JOIN classes ON entries.class_id = classes.id").where(・・・)
<!-- class/show.html.erb -->
<% @students.each do | student | %>
    :
<td><span><%= student.entries.present? ? 'あり' : 'なし' %></span></td>
SELECT `entries`.* FROM `entries` WHERE `entries`.`student_id` = 1 AND (entries.deleted_at IS NULL)
SELECT `entries`.* FROM `entries` WHERE `entries`.`student_id` = 2 AND (entries.deleted_at IS NULL)
SELECT `entries`.* FROM `entries` WHERE `entries`.`student_id` = 3 AND (entries.deleted_at IS NULL)
    :

ビューをstudent.entries.exists?(:class_id => @class.id)みたいに書き換えると結果そのものは正常になりますが、N+1 問題が起きるのはなんとかしてしまいたいところです。

students = Studens.includes(:entries => {:class_id => class.id}).where(・・・)

こういう風にかけると解決するんですが。。。

こんなとき、Rails ではどのように解決するのが一般的なのでしょうか。
今のところは上記のように、ビューで各生徒ごとに対象の授業を履修しているか取得する以外の方法を思いつきません。
パフォーマンス上ネックになっているところなので、解決策を教えてください。

回答

select を使って下記のように書くのはいかがでしょうか?
※ Class / class だと書きにくいので Lesson / lesson に変えています

# MySQL での例
class Student < ActiveRecord::Base
  def self.with_entry_id_of_lesson(lesson)
    select("students.*, entries.id AS entry_id").joins(<<-SQL
      LEFT OUTER JOIN entries
        ON entries.student_id = students.id
        AND entries.lesson_id = #{lesson.id.to_i}
    SQL
    )
  end
end

class LessonsController < ActionContorller::Base
  def show
    @lesson = Lesson.find(params[:id])
    @students = Student.with_entry_id_of_lesson(@lesson)
  end
end
<% @students.each do |student| %>
    :
<td><span><%= student.entry_id ? 'あり' : 'なし' %></span></td>
編集 履歴 (0)
  • select使うやり方はjoinsと組み合わせると行けそうですね!includesと組み合わせようとしてダメじゃんって諦めていたのでした。試してみて結果をまたご報告します。 -
  • やってみましたが、selectの内容は完全スルーされました。。。(泣)
    発行されるSQLではstudentの中身は取得するものの、entries.idの方は無視。
    ビューで<%= student['entry_id'] %>などとしてみましたが、表示できないです。
    -
  • ちなみに、selectとincludesとjoins三つで試したりもしてみましたが、これもダメ。どうもこの辺りが理由のようです。
    https://github.com/rails/rails/issues/4564
    -
  • 提示された issue だと includes を使ったときに問題あるとのことですが、回答で挙げた例では includes を使っていませんので、どこで問題が起こっているのかわかりません。includes 使わずに書く事はできないでしょうか。 -
  • 回答いただいたコードを試してもentry_idを取得するSQLが発行されませんでした。joins側のSQLはちゃんと反映されるのですが(21:54のコメント)。なのでincludesを組み合わせたらどうかな?と試してみた次第です(21:56のコメント)。labochoさんの環境で成功するのなら、私の環境の問題か、コードの反映が不十分なのかもしれません。もう少し調べてみます。 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。