Rails 5 слева внешнего соединения, используя includes () и где ()

У меня есть время, затрачиваемое на предполагаемое поведение, включая include () и where ().

Результат Я хочу:
- Все студенты (даже если они имеют нулевые отметки)
- Все проверки в библиотеке

Результат, который я получаю:
- Только студенты с регистрационными записями в библиотеке
- Все проверки в библиотеке, для тех студентов


В настоящее время мой код основан на этом: http://edgeguides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations

Который описывает поведение, которое я хочу:

Article.includes(:comments).where(comments: { visible: true })

Если в случае с этим запросом не было комментариев для каких-либо статей, все статьи все равно будут загружены.

Мой код:

@students = Student.includes(:check_ins)
                    .where(check_ins: {location: "Library"})
                    .references(:check_ins)

,

class CheckIn < ApplicationRecord
  belongs_to :student
end

,

class Student < ApplicationRecord
  has_many :check_ins, dependent: :destroy
end

Сгенерированный запрос SQL:

SELECT "students"."id" AS t0_r0,"check_ins"."id" AS t1_r0, "check_ins"."location" AS t1_r1, "check_ins"."student_id" AS t1_r6 FROM "students" LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id" WHERE "check_ins"."location" IN ('Library')

Этот SQL-запрос дает поведение соединения, которое я хочу:

SELECT first_name, C.id FROM students S LEFT OUTER JOIN check_ins C ON C.student_id = S.id AND location IN ('Library');

mysql,ruby-on-rails,activerecord,left-join,ruby-on-rails-5,

2

Ответов: 3


2 принят

Пробовал новый подход, используя Scopes с отношениями, ожидая предварительной загрузки всего и отфильтровывая его, но был приятно удивлен тем, что Scopes фактически дает мне точное поведение, которое я хочу (вплоть до нетерпеливой загрузки).

Вот результат:

Этот вызов ActiveRecord вытаскивает полный список студентов и загружает записи:

@students = Student.all.includes(:check_ins)

Объем класса Student < ApplicationRecord has_many : check_ins , -> { где ( 'место =' Библиотека ' }, зависимый : : уничтожить конец может быть ограничено право в декларации has_many:

  Student Load (0.7ms)  SELECT "students".* FROM "students"
  CheckIn Load (1.2ms)  SELECT "check_ins".* FROM "check_ins" WHERE location = 'Library') AND "check_ins"."student_id" IN (6, 7, 5, 3, 1, 8, 9, 4, 2)

Результат состоит из двух чистых, эффективных запросов:

Student.joins("LEFT OUTER JOIN check_ins ON check_ins.student_id = students.id AND check_ins.location = 'Library'")


Бинго!

ps вы можете больше узнать об использовании областей с ассоциациями здесь:
http://ducktypelabs.com/using-scope-with-associations/


1

Я думаю, что это единственный способ создать требуемый запрос.

LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
  AND location IN ('Library')

Ссылка: http://apidock.com/rails/ActiveRecord/QueryMethods/joins


1

Что вы хотите с точки зрения чистого SQL:

class Student < ApplicationRecord
  has_many :check_ins

  def self.joins_check_ins
    joins( <<~SQL
      LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
      AND location IN ('Library')
    SQL
    )
  end
end

Однако невозможно (afaik) получить ActiveRecord, чтобы отметить ассоциацию как загруженную без обмана * .

irb(main):041:0> Student.joins_check_ins.map {|s| s.check_ins.loaded? }
  Student Load (1.0ms)  SELECT "students".* FROM "students" LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
AND location IN ('Library')
=> [false, false, false]

irb(main):042:0> Student.joins_check_ins.map {|s| s.check_ins.size }
  Student Load (2.3ms)  SELECT "students".* FROM "students" LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
AND location IN ('Library')
   (1.2ms)  SELECT COUNT(*) FROM "check_ins" WHERE "check_ins"."student_id" = $1  [["student_id", 1]]
   (0.7ms)  SELECT COUNT(*) FROM "check_ins" WHERE "check_ins"."student_id" = $1  [["student_id", 2]]
   (0.6ms)  SELECT COUNT(*) FROM "check_ins" WHERE "check_ins"."student_id" = $1  [["student_id", 3]]

Поэтому, если мы итерируем результат, это вызовет проблему с N + 1:

check_ins

Честно говоря, мне никогда не нравятся предварительные загрузки только подмножества ассоциаций, потому что некоторые части вашего приложения, вероятно, предполагают, что он полностью загружен. Это может иметь смысл только в том случае, если вы получаете данные для его отображения.
- Роберт Панковецки, 3 способа сделать загрузку (предварительную загрузку) в Rails 3 и 4

Поэтому в этом случае вы должны рассмотреть предварительную загрузку всех данных и использовать что-то вроде подзапроса, чтобы выбрать счетчик check_ins.

Я также советую вам создать отдельную таблицу для местоположений.

MySQL, рубин-на-рельсы, ActiveRecord, левый присоединиться, рубин-на-рельсы-5,
Похожие вопросы