我有一段时间使用includes()和where()获得预期的行为。
我想要的结果:
- 所有学生(即使他们没有办理登机手续)
- 图书馆中的所有签到
结果我得到了:
- 只有在图书馆办理登机手续的学生
- 图书馆的所有签到,为那些学生
其中描述了我想要的行为:
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');
答案 0 :(得分:2)
您想要的纯SQL是:
LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
AND location IN ('Library')
但是,无法(afaik)让ActiveRecord将关联标记为已加载without trickery*。
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]]
老实说,我从不喜欢仅预加载关联子集 因为你的应用程序的某些部分可能会认为它是 完全读取。它可能只有你得到数据才有意义 显示它。
- Robert Pankowecki,3 ways to do eager loading (preloading) in Rails 3 & 4
因此,在这种情况下,您应该考虑预加载所有数据并使用subquery之类的内容来选择check_ins
的数量。
我还建议你为地点创建一个单独的表格。
答案 1 :(得分:2)
尝试使用带有关系的Scopes的新方法,期望预加载所有内容并过滤掉它,但令人惊喜的是Scopes实际上给了我我想要的确切行为(直到急切加载)。
结果如下:
此ActiveRecord Call会提取完整的学生列表并急切地加载签到:
@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/
答案 2 :(得分:1)
我认为这是创建所需查询的唯一方法。
Student.joins("LEFT OUTER JOIN check_ins ON check_ins.student_id = students.id AND check_ins.location = 'Library'")