Rails 5使用includes()和where()离开外连接

时间:2016-10-12 13:38:34

标签: mysql ruby-on-rails activerecord left-join 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 })
     

如果在此包含查询的情况下,则没有任何评论   文章,所有文章仍然会被加载。

我的代码:

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

3 个答案:

答案 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'")

参考:http://apidock.com/rails/ActiveRecord/QueryMethods/joins