Rails数据预加载 includes eager_load preload

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

原文章地址请点在这里

三种预加载方式

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

假如

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

1
user has_many posts

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

preload

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

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

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

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

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

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

eager_load

因为sql太长所以分了几行

1
2
3
4
5
6
7
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加上条件查询语句的,即:

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

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

1
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中使用,就会报错,报错信息里面有着详细解释,截取一段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
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中加入

1
has_many :popular_posts, condition: {like_count > 100}, class_name: 'Post'

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

1
2
3
4
5
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效果

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