Rails Rails 巧用 preload、 eager_load、(includes + references) 和 joins

Abel.sun · August 25, 2018 · 2727 hits

Rails 巧用 preload、eager_load、(includes + references)和 joins

Rails 框架中对于加载表关联数据一共提供了四种方法(preload、eager_load、includes 和 joins)下面我就来说一说这几个方法。如果有什么出处,欢迎大家帮忙指出。

我经常看到在 Rails 项目中处理 SQLN+1 的问题上,很多人都用的很懵懂,就是一个只要碰到 SQLN+1 我就用一个主 model,然后把所有与他相关的关联的 model 都用 includes 加 references 全部加起来,这样会造成一个 Sql 效率的问题,下面咱们就来看看这四种方法的效果吧

首先解释下 preload 和 eager_load

preload 总会生成多条附加的查询语句来加载关联的数据

演示条件

  • 假设有两个表
表名 model 名
orders Order
users User
class Order < ApplicationRecord
  belongs_to :user
end
class User < ApplicationRecord
end

orders = Order.preload(:user)

# => 
SELECT "orders".* FROM "orders"
SELECT "users".* FROM "users"  WHERE "users"."id" IN (1)

切记 preload 括号后跟几个关联对象他就会生成几条附加的查询语句 preload 会将与 Order 相关的表的数据统一加载出来放入并付给他所接收的对象,比如上面代码 orders,但是这只会让你 orders.user.name 的时候不会额为的生成 SQL,只要在 preload 中传入关联对象的名称他都不会再生成额外的 SQL,从一定程度上解决 SQLN+1 的问题,但是,当你执行 where 查询的时候可能就不行了,咱们下面来看看。

orders = Order.preload(:user).where("users.name like '%Abel%'")

# =>
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'users.name' in 'where clause': SELECT `orders`.* FROM `orders` WHERE (users.name like '%Abel%')

诺,由于它是生成多条单独的 SQL 查询语句,SQL 语句中没有建立关联关系,所以这样的写法所生成的 SQL 语句在数据库层就会抛出异常。不过如果是一些简单的查询,给大家提供一个关于 preload 的一个小窍门

orders = Order.preload(:user).where(users: {name: 'Abel'})

这样在 rails 层就能识别 where 中的信息并且将它压缩成一条 SQL 语句发向数据库层去执行,不过遗憾的是这样貌似只能支持精确匹配。大家可以下去玩玩,我就不过多详述了。

eager_load 使用 LEFT OUTER JOIN 进行单次查询,并加载所有的关联数据。

演示条件同上:

  • 同上
class Order < ApplicationRecord
  belongs_to :user
end
class User < ApplicationRecord
end

orders = Order.eager_load(:user)

# =>
SELECT  "orders"."id" AS t0_r0, "orders"."order_amount" AS t0_r1, "orders"."created_at" AS t0_r2, "orders"."updated_at" AS t0_r3, "users"."id" AS t1_r0, "users"."name" AS t1_r1, "users"."created_at" AS t1_r2, "users"."updated_at" AS t1_r3 FROM "orders" LEFT OUTER JOIN "users" ON "users"."id" = "orders"."user_id"

它会根据 LEFT OUTER JOIN 方式生成 SQL 语句并且将与之相关联表的数据统一加载到一个对象中,所带来的展示效果通 preload 的效果同样减少 SQLN+1 的问题,且支持orders = Order.eager_load(:user).where("users.name like '%Abel%'")的写法,但是这样由于 eager_load 中加载的关联对象越多生成的 SQL 语句就越复杂,那么就会造成 SQL 的运行效率低下的问题。这个大家可以尝试下

下来来介绍一下我今天着重想说的关于 includes+references 和 joins 吧

includes

includes 的默认效果跟 preload 的效果一样,我就不详说了。我就主要说说 includes + references 组合一块的用法吧。

includes + references 的效果类似于 eager_load,但是他比 eager_load 更灵活,为什么呢?来一起撸下代码吧

class Order < ApplicationRecord
  belongs_to :user
end
class User < ApplicationRecord
end

orders = Order.includes(:user).where("users.name like '%Abel%'").references(:user)

# =>
SELECT  "orders"."id" AS t0_r0, "orders"."order_amount" AS t0_r1, "orders"."created_at" AS t0_r2, "orders"."updated_at" AS t0_r3, "users"."id" AS t1_r0, "users"."name" AS t1_r1, "users"."created_at" AS t1_r2, "users"."updated_at" AS t1_r3 FROM "orders" LEFT OUTER JOIN "users" ON "users"."id" = "orders"."user_id" WHERE (users.name like '%Abel%')

乍一看跟 eager_load 差不多,但是要注意啦,includes + references 组合有个特点,就是你 references() 中加载几个对象,那么它就只会加载几个关联数据到接收者对象中,来假设再有一张表 order_supplements, 再来看看代码

class Order < ApplicationRecord
  belongs_to :user
  belongs_to :order_supplement
end
class User < ApplicationRecord
end

orders = Order.includes(:user, :order_supplement).where("users.name like '%Abel%'").references(:user)

orders.last.user.name
# => 它就会直接输出与user中的name

orders.last.order_supplement.id
#  =>
SELECT  `order_supplements`.* FROM `order_supplements` WHERE `order_supplements`.`order_id` = 1
# 1

瞧瞧上面的代码,你们发现不同了吗?是的这两个的组合只会将 references 中加载的队形的数据存入接收者对象之中。

joins

class Order < ApplicationRecord
  belongs_to :user
  belongs_to :order_supplement
end
class User < ApplicationRecord
end

orders = Order.joins(:user, :order_supplement)
# => SELECT  "orders".* FROM "orders" INNER JOIN "users" ON "users"."id" = "orders"."user_id" INNER JOIN "order_supplements" ON "order_supplements"."id" = "orders"."order_supplement_id"

joins 与上面的不同之处在于它是 INNER JOIN 来加载关联数据,并且“永远不会”将关联数据加载到接收者对象中,所以它如果每点一次与它相关联的数据的时候就会重复的生成一条 SQL。解释下为什么要将永远不会用“”圈起来,在 preload 和 includes 或 includes +references 中,如果接收者对象中没有相对应关系的数据时,它只会生成一条 SQL 去数据库中进行检索,之后就会将相关数据加载到接收者对象中,不会在重复的检索数据库。

joins 只会生成一条 inner join 的查询语句,但是内连接有个操蛋的点,就是会生成重复的数据,不过可以采用 uniq 的方式去除重复的数据,但是还是感觉很蛋疼,哎,有兴趣的同学可以尝试下数据库层的 inner join 在一对多和多对多的时候生成的数据,我这里就不详述了。

# 内连接使用 uniq 去重方法, 大家可以试下
orders = Order.joins(:user, :order_supplement).uniq
No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.