Rails 5 левое внешнее соединение с использованием includes() и where()

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

#mysql #ruby-on-rails #activerecord #левое соединение #ruby-on-rails-5

Вопрос:

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

Результат, который я хочу:
— Все учащиеся (даже если у них нулевые проверки)
— Все проверки в библиотеке

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

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

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

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

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

Мой код:

 @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');
  

Ответ №1:

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

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

Этот вызов ActiveRecord извлекает полный список учащихся и с нетерпением загружает проверки:

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

Область check_ins может быть ограничена прямо в объявлении has_many:

 Class Student < ApplicationRecord
    has_many :check_ins, -> {where('location = 'Library'}, dependent: :destroy
end
  

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

   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)
  

Бинго!

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

Комментарии:

1. {where('location = 'Library'} => {where("location = 'Library'")} или {where(location: 'Library')}

Ответ №2:

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

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

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

 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
  

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

 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]]
  

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

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

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

Комментарии:

1. Подзапрос будет выглядеть примерно так (SELECT count('check_ins.location') WHERE 'check_ins.location' IN ?) AS students.check_in_count . Я годами не касался MySQL и не устанавливал его, чтобы попробовать.

2. Вы человек, после более чем часа я нашел ваш код соединения, который решает для меня большую проблему!

Ответ №3:

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

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

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

Комментарии:

1. Моя цель — быстро загрузить ассоциации, чтобы минимизировать количество запросов. Хотя этот подход позволяет получить правильные данные, для его получения требуется много запросов.