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