Rails Rails 的 includes 预加载的实际调用

391117134 · 2015年08月17日 · 最后由 liyaodonglibin 回复于 2015年08月18日 · 3445 次阅读

前些天因为想减少站点的 sql 查询,过程中遇到些问题,搜解决办法时看到一篇文章,受益匪浅,想把自己的看完的理解总结然后分享一下,如果有不对的地方,求指导。

原文章地址请点在这里

三种预加载方式

我们在 rails 中有三种方式可以用来预加载数据,分别是 eager_load preload includes. 但是实际上预加载的方式就只有两种,preload 和 eager_load, 而 includes 是做什么的呢?它是内部自行判断,然后调用 preload 或是 eager_load。

假如

有个 User(用户) model 还有一个 Post(帖子) model,它们是一对多的关系

user has_many posts

首先先看看使用 preload 和 eager_load 这两中加载方法有什么区别

preload

使用 preload 将查出用户的时候预加载出用户发的帖子

User.preload(:posts)
#select “users”.* from “users"
#select “posts”.* from “posts” where “posts”. “user_id” IN (1, 2, 3)

它生成两句 sql 语句,第二句预加载了 posts 表的内容。但是这种方式有个弊端,当你在预加载的时候想给预加载的表加上条件的时候就有问题了,比如我想加载用户的同时加载这个用户点赞高于 100 的例子

User.preload(:posts).where("posts.like_count > ?", 100)
#报错

因为 preload 这种加载方式是使用两句查询语句,并没有 join posts 表,所以不能这样给 post 加上条件

想要给预加载的表加上条件,那就试试使用 eager_load 的方式来预加载

eager_load

因为 sql 太长所以分了几行

User.eager_load(:posts)

#select "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."email" AS t0_r2,

#"posts"."id" AS t1_r0, "posts"."title" AS t1_r1,"posts"."like_count" AS t1_r2, "posts"."users_id" AS t1_r3

#from "users" left outer join "posts" on "posts"."users_id" = "users"."id"

可以看到最后一行 eager_load 预加载数据是将 users 和 posts 做了左连接,所以 eager_load 是可以给 post 加上条件查询语句的,即:

User.eager_load(:posts).where("posts.like_count > ?", 100)

生成的 sql 语句为,前面两行相等,最后一行多了个查询条件:

from "users" left outer join "posts" on "posts"."users_id" = "users"."id" where (posts.like_count > 100)

看,条件查询就可以正常使用了。其实 preload 也是可以实现条件加载,下面会说到。

includes

最后说一下 inludes, 在 Rails3 中 includes 它是可以自行判断这种一对多的预加载条件查询的,即当你使用

User.includes('posts') 它会调用 User.preload('posts') 当使用 User.includes('posts').where("posts.like_count > ?", 100) 它会自行调用 User.eager_load('posts').where("posts.like_count > ?", 100)

但是在 Rails4 中使用,就会报错,报错信息里面有着详细解释,截取一段

User.includes('posts').where("posts.like_count > ?", 100)

#DEPRECATION WARNING: It looks like you are eager loading table(s)
# (one of: users, addresses) that are referenced in a string SQL
# snippet. For example:
#
#    Post.includes(:comments).where("comments.title = 'foo'")
#
# Currently, Active Record recognizes the table in the string, and knows
# to JOIN the comments table to the query, rather than loading comments
# in a separate query. However, doing this without writing a full-blown
# SQL parser is inherently flawed. Since we don't want to write an SQL
# parser, we are removing this functionality. From now on, you must explicitly
# tell Active Record when you are referencing a table from a string
#
#   Post.includes(:comments).where("comments.title = 'foo'").references(:comments)
#

原来这种自行判断的方法在 Rails4 时被去掉了,在预加载对象时使用判断语句,它还是会使用 preload。

有两个办法解决:

一个是直接调用 eager_load

另一个是像上面报错信息里面举得例子一样使用 references

User.includes('posts').where("posts.like_count > ?", 100).references(:posts)
#或是
User.preload('posts').where("posts.like_count > ?", 100).references(:posts)

两个方法生成的 sql 语句都是一样的,都是左连接查询

结合使用 association 来实现预加载

其实 preload 或是 eager_load 还可以通过 association 实现预加载

例如: 在 user 的 model 中加入

has_many :popular_posts, -> {where "like_count > 100"}, class_name: 'Post'

使用 preload 来预加载会生成两条 sql 语句来查询

User.preload(:popular_posts)
#生成的sql语句
#select "users".* from "users"
#select "posts".* from "posts" where "posts"."like_count" > 100 AND "posts"."user_id" in (1, 2, 3)
#

使用 eager_load 效果

User.eager_load(:popular_posts)
#生成sql语句和下面一样,都是左链接
User.eager_load(:posts).where("posts.like_count > ?", 100)

还可以用joins

User.joins(:posts).where('...')

对使用 joins 也可以达到同样的效果

#1 楼 @flemon #2 楼 @391117134

JOIN 是inner join哟!和left join 的效果可不一样...

#1 楼 @flemon joins 只是查询条件,并没有预加载的效果。

@hooopo 恩 joins 默认使用的是内连接查询,也可以通过下面方式实现外接查询

User.joins("left outer join posts on users.id=posts.user_id").where("....")

关于 结合使用 association 来实现预加载 这一节的描述,语法已经过时了,目前的 rails 版本是不认 condition 的,直接使用 lambda 作为第二个参数即可: has_many :popular_posts, -> {like_count > 100}, class_name: 'Post'

@scriptfans 感谢指出,已将错误修改

#8 楼 @391117134 sorry,忘记加 where 了 has_many :popular_posts, -> {where 'like_count > 100'}, class_name: 'Post' 这个才对:)

... 已更正,thank you

楼主这种刨根问底的精神值得学习啊!

需要 登录 后方可回复, 如果你还没有账号请 注册新账号