#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. Моя цель — быстро загрузить ассоциации, чтобы минимизировать количество запросов. Хотя этот подход позволяет получить правильные данные, для его получения требуется много запросов.