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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容