Return 来自 has_many 关联的过滤后的每一行

Return every row from has_many association after filter

拥有模型 Author 和 Book,其中 Author has_many 本书,以及 Author 的范围,例如:

scope :by_books,->(book_ids) { joins(:books).where(books: { id: book_ids }) }

为了过滤作者,如果他们是给定书籍的作者,则仅在以下结果中显示作者和指定书籍:

Author.by_books(1).includes(:books).as_json(include: :books)

而我希望在生成的 json 中包含作者的所有书籍。我可以通过使用 .joins(:books) 而不是 .includes(:books) 来做到这一点,但现在我对连接和包含的作用完全感到困惑。我想知道是否有人可以给我一个解释,我的期望哪里出了问题。

(给出的代码是我实际的模拟,在语法方面可能有缺陷,但我相信行为是相同的)

  • joins 是连接 2 个表的纯 SQL 词查询。
  • includes 用于预先加载(避免 n+1 查询问题)

class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
end
Author.create!([
  { name: 'Author 1',
    books: [ Book.new(name: 'Book 1 of A1'), Book.new(name: 'Book 2 of A1') ] },
  { name: 'Author 2',
    books: [ Book.new(name: 'Book 1 of A2') ] },
  { name: 'Author 3' } 
])

你必须小心 joinsincludes,它们 return 不同的结果取决于关联。

我们 'Author 1' 有两本书,'Author 2' 有一本书,'Author 3' 没有书。

joins 执行 INNER JOIN 并根据数据库结果实例化新对象,这可能 return 重复记录而不是 return 没有关联的记录

>> Author.joins(:books)
  Author Load (1.3ms)  SELECT "authors".* FROM "authors" INNER JOIN "books" ON "books"."author_id" = "authors"."id"
=> [ #<Author:0x00007f0dd278d818 id: 1, name: "Author 1">,
     #<Author:0x00007f0dd278d688 id: 1, name: "Author 1">, # duplicate
     #<Author:0x00007f0dd278d598 id: 2, name: "Author 2">] 
   # 'Author 3' is not in the result

includesN+1 的 rails 解决方案。它运行两个查询 preload 或一个查询 eager_load

>> Author.includes(:books)
  Author Load (0.8ms)  SELECT "authors".* FROM "authors"
  Book Load (1.1ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" IN (, , )  [["author_id", 1], ["author_id", 2], ["author_id", 3]]
=> [ #<Author:0x00007f0dd27e59c8 id: 1, name: "Author 1">, 
     #<Author:0x00007f0dd27e5900 id: 2, name: "Author 2">, 
     #<Author:0x00007f0dd27e5838 id: 3, name: "Author 3">]

当您有条件时,它会在单个查询中执行 eager_load

# same as  Author.eager_load(:books)
>> Author.includes(:books).references(:books)
  SQL (1.0ms)  SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, "books"."id" AS t1_r0, "books"."name" AS t1_r1, "books"."author_id" AS t1_r2 FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id"                                                          
=> [ #<Author:0x00007f0dd283a1f8 id: 1, name: "Author 1">, 
     #<Author:0x00007f0dd2839bb8 id: 2, name: "Author 2">, 
     #<Author:0x00007f0dd2839780 id: 3, name: "Author 3">]

这样你就可以得到所有有或没有书的作者,书籍对象被预加载到作者对象中。

>> Author.includes(:books).to_a.first.instance_variable_get('@association_cache')
  Author Load (0.7ms)  SELECT "authors".* FROM "authors"
  Book Load (0.7ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" IN (, , )  [["author_id", 1], ["author_id", 2], ["author_id", 3]]
=> {:books=>
  #<ActiveRecord::Associations::HasManyAssociation:0x00007f0dd28ff9d0
   @association_ids=nil,
   @association_scope=nil,
   @disable_joins=false,
   @loaded=true,
   @owner=#<Author:0x00007f0dd28f83b0 id: 1, name: "Author 1">,
   @reflection=#<ActiveRecord::Reflection::HasManyReflection:0x00007f0dd58a3f38 ... >,
   @replaced_or_added_targets=#<Set: {}>,
   @stale_state=nil,
   @target=[ #<Book:0x00007f0dd28fc230 id: 1, name: "Book 1 of A1", author_id: 1>, 
             #<Book:0x00007f0dd28fc078 id: 2, name: "Book 2 of A1", author_id: 1>]>}
  #          ^
  # books are preloaded

>> Author.includes(:books).to_a.last.instance_variable_get('@association_cache')
  Author Load (0.9ms)  SELECT "authors".* FROM "authors"
  Book Load (0.7ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" IN (, , )  [["author_id", 1], ["author_id", 2], ["author_id", 3]]
=> {:books=>
  #<ActiveRecord::Associations::HasManyAssociation:0x00007f0dd2a017c0
   ...,
   @target=[]>}
  #        ^
  # doesn't have any books

当 include 执行 eager_load 时,它会运行 LEFT OUTER JOIN,因此您可以获得所有作者并且 rails 合并重复的结果,这与 joins 方法不同。

>> ActiveRecord::Base.connection.execute(Author.eager_load(:books).to_sql).to_a
   (0.8ms)  SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, "books"."id" AS t1_r0, "books"."name" AS t1_r1, "books"."author_id" AS t1_r2 FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id"                                                                                                                                                          
=>
[{"t0_r0"=>1, "t0_r1"=>"Author 1", "t1_r0"=>1, "t1_r1"=>"Book 1 of A1", "t1_r2"=>1},
 {"t0_r0"=>1, "t0_r1"=>"Author 1", "t1_r0"=>2, "t1_r1"=>"Book 2 of A1", "t1_r2"=>1}, 
# ^ duplicate `Author 1` like the `joins` method
 {"t0_r0"=>2, "t0_r1"=>"Author 2", "t1_r0"=>3, "t1_r1"=>"Book 1 of A2", "t1_r2"=>2},
 {"t0_r0"=>3, "t0_r1"=>"Author 3", "t1_r0"=>nil, "t1_r1"=>nil, "t1_r2"=>nil}]       

'Author 1' 结果合并到一个 Author 对象中。

您同时是 运行 joinsincludes,它们结合起来执行 eager_load 样式查询。

Author.joins(:books).includes(:books).where(books: {id: [1,2]})
 # INNER JOIN 
 # 2 database results
 # 1 Author object 
 # Books are preloaded
Author.joins(:books).where(books: {id: [1,2]})
 # INNER JOIN
 # 2 database results
 # 2 Author objects
 # No books are loaded
Author.includes(:books).where(books: {id: [1,2]})
 # LEFT OUTER JOIN
 # 2 database results
 # 1 Author object 
 # Books are preloaded