前些天因为想减少站点的 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 将查出用户的时候预加载出用户发的帖子
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 的方式来预加载
因为 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 也是可以实现条件加载,下面会说到。
最后说一下 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 语句都是一样的,都是左连接查询
其实 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)