简体   繁体   English

Rails 5 使用includes() 和where() 进行左外连接

[英]Rails 5 left outer join using includes() and where()

I'm having a heck of a time getting the intended behavior using includes() and where().我有很多时间使用includes() 和where() 来获得预期的行为。

Result I want:我想要的结果:
- All students (even if they have zero check-ins) - 所有学生(即使他们零签到)
- All check-ins in the Library - 图书馆中的所有签到

Result I'm getting:结果我得到:
- Only students with check-ins in the library - 只有在图书馆签到的学生
- All check-ins in the library, for those students - 所有在图书馆签到的学生,对于那些学生


Currently my code is based off of this: http://edgeguides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations目前我的代码基于此: http : //edgeguides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations

Which describes the behavior I want:它描述了我想要的行为:

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

If, in the case of this includes query, there were no comments for any articles, all the articles would still be loaded.如果在这种包含查询的情况下,没有任何文章的评论,则仍会加载所有文章。

My code:我的代码:

@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

The generated SQL query:生成的 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')

This SQL query gives the join behavior I want:这个 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');

Tried a new approach using Scopes with relations, expecting to preload everything and filter it out, but was pleasantly surprised that Scopes actually give me the exact behavior I want (right down to the eager loading).尝试使用带有关系的 Scopes 的新方法,期望预加载所有内容并将其过滤掉,但惊喜的是 Scopes 实际上给了我我想要的确切行为(一直到急切加载)。

Here's the result:结果如下:

This ActiveRecord Call pulls in the full list of students and eager loads the check-ins:此 ActiveRecord 调用会拉入完整的学生列表并急切地加载签到:

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

The scope of check_ins can be limited right in the has_many declaration: check_ins 的范围可以在 has_many 声明中进行限制:

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

Resulting in two clean, efficient queries:产生了两个干净、高效的查询:

  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)


Bingo!答对了!

ps you can read more about using scopes with assocations here: ps,您可以在此处阅读有关将作用域与关联一起使用的更多信息:
http://ducktypelabs.com/using-scope-with-associations/ http://ducktypelabs.com/using-scope-with-associations/

What you want in terms of pure SQL is:就纯 SQL 而言,您想要的是:

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

However it is not possible (afaik) to get ActiveRecord to mark the association as loaded without trickery* .但是,不可能(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

So if we iterate though the result it will cause a N+1 query issue:因此,如果我们迭代结果,它将导致 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]]

To be honest, I never like preloading only a subset of association because some parts of your application probably assume that it is fully loaded.老实说,我从不喜欢只预加载关联的一个子集,因为您的应用程序的某些部分可能会假设它已完全加载。 It might only make sense if you are getting the data to display it.只有在获取数据以显示它时才有意义。
- Robert Pankowecki, 3 ways to do eager loading (preloading) in Rails 3 & 4 - Robert Pankowecki,在 Rails 3 和 4 中进行预加载(预加载)的 3 种方法

So in this case you should consider preloading all the data and using something like a subquery to select the count of check_ins .因此,在这种情况下,您应该考虑预加载所有数据并使用子查询之类的内容来选择check_ins的计数。

I would also advise you to create a separate table for locations.我还建议您为位置创建一个单独的表。

I think this is the only way to create the query you want.我认为这是创建所需查询的唯一方法。

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM