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' }
])
你必须小心 joins
和 includes
,它们 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
includes
是 N+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
对象中。
您同时是 运行 joins
和 includes
,它们结合起来执行 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
拥有模型 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' }
])
你必须小心 joins
和 includes
,它们 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
includes
是 N+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
对象中。
您同时是 运行 joins
和 includes
,它们结合起来执行 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