joins、includes、preload、eager_load、references的关系
joins
joins的使用场景应是:过滤,并且你不打算获取所关联对象的属性。如下例子,获取全部Derek有comment过的post,但并不读取comment的属性
Post.joins(:comments).where(:comments => {author: 'Derek'}).map { |post| post.title }
Post Load (1.2ms) SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "comments"."author" = $1
=> ["One weird trick to better Rails apps",
"1,234 weird tricks to faster Rails apps",
"You wouldn't believe what happened to this Rails developer after 14 days"]
它能避免N+1问题吗?
不能,因为它并不加载所关联对象的属性到内存中,这时如果访问关联对象的属性,就会再发起查询,如下
Post.joins(:comments).where(:comments => {author: 'Derek'}).map { |post| post.comments.size }
Post Load (1.2ms) SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "comments"."author" = $1
(1.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(3.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(0.3ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(1.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(2.1ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(1.4ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
=> [3,5,2,4,2,1]
joins能否与includes、preload、eager_load一起使用?
能。joins的连接方法(默认是INNER JOIN)会优先于includes、eager_load。而preload不作连接
此外,与left_outer_joins一样,它返回的结果集是不会归并到父对象的,即子表返回多少,父对象就有多少个(对于left_outer_joins,父对象至少一个)
includes
它能避免N+1问题吗?
能,它先加载父对象的所有记录,然后加载那些传给includes方法的参数所表示的关联对象
从以下例子可注意到,它只发起一条额外的查询。刚才的joins与这对比来看,则是每个post都有一次count查询
Post.includes(:comments).map { |post| post.comments.size }
Post Load (1.2ms) SELECT "posts".* FROM "posts"
Comment Load (2.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 3, 4, 5, 6)
=> [3,5,2,4,2,1]
它是否总会发起另一条查询来加载关联对象?
不是,如果你有对关联对象进行where或order,则它会用LEFT OUTER JOIN,如下
Article.includes(:comments).where(comments: { visible: true })
SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1)
(注意,对于没有任何comment的article,是不会在上面的结果集中出现的,因为条件被放在了WHERE中而不是ON中)
如何避免进一步的N+1问题?这样:
Article.includes(:comments => :user)
Article Load (0.3ms) SELECT "articles".* FROM "articles"
Comment Load (0.6ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" IN (3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (4, 3, 5, 6, 2)
preload/eager_load与includes的关系
preload是发起两条查询的,而eager_load则是用LEFT OUTER JOIN(因此可过滤“主表”和关联表的数据)
includes会在有where和order、references时转用eager_load,否则效果如同preload
preload不能在where中带有所关联表的过滤条件,因为这个条件会被拼到第一条语句(查父表)上,但这个语句并没连接子表
eager_load的LEFT OUTER JOIN会产生多余的父对象数据,虽然最后会归并到一个父对象上
带条件的preload
如果想查出所有user的同时,预加载各user的like_count > 100的post,可这样
# in class User
has_many :popular_posts, -> {where "like_count > 100"}, class_name: 'Post'
User.preload(:popular_posts)
#select "users".* from "users"
#select "posts".* from "posts" where "posts"."like_count" > 100 AND "posts"."user_id" in (1, 2, 3)
不过,这样条件就写死了
如果想动态设置,可试试类似如下
class Article < ApplicationRecord
has_many :comments
def self.preload_comments_by user
accessor = define_preload_comments_by_if_not_yet user
preload accessor
end
def self.define_preload_comments_by_if_not_yet user
accessor_str = "comments_by_#{user.id}"
accessor_sym = accessor_str.to_sym
unless _reflections.has_key? accessor_str
(@lock ||= Mutex.new).synchronize do
unless _reflections.has_key? accessor_str
puts accessor_str
has_many accessor_sym, -> { where user: user }, class_name: Comment
end
end
end
accessor_sym
end
end
使用方法
irb(main):008:0> current_user = User.find(3) # current_user可用before_action查出
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
irb(main):009:0> Article.preload_comments_by(current_user).page(2).per(5)
Article Load (0.2ms) SELECT "articles".* FROM "articles" LIMIT ? OFFSET ? [["LIMIT", 5], ["OFFSET", 5]]
Comment Load (0.3ms) SELECT "comments".* FROM "comments" WHERE "comments"."user_id" = 3 AND "comments"."article_id" IN (8, 9, 10, 11, 12)
references
需与includes一起使用
如果includes带的where是含有关联表过滤条件的字符串形式,需用references指定查的是那个表,这时它实际用的是eager_load。(不过,references的参数好像可以随便指定表名,甚至随便什么名字,都可以……)
总结
简单关联、过滤,用joins
需要预加载,用includes
不想用两条查询来预加载,指定eager_load
到底应该用preload作两条查询还是一条eager_load作LEFT OUTER JOIN,需看实际情况,而includes在不涉及子表条件时,倾向于使用preload