您的位置:首页 > 数据库

ActiveRecord 的数据三种预加载形式 - includes, preload, eager_load和joins(不是预加载)

2017-07-23 01:17 597 查看
引言:在平常的ActiveRecord的开发中使用sql语句方式有很多,那么ActiveRecord有没有提供我们一些便利的方式来实现这个魔法,其实有的,想知道详情请向下看。

当然在开始之前我们准备下数据:

class Province < ActiveRecord::Base
has_many :cities
has_many :viliages
end

class City < ActiveRecord::Base
belongs_to :province
has_many :viliages
end

class Viliage < ActiveRecord::Base
belongs_to :city
belongs_to :province
end


Joins:

=>rails 中 joins 产生的均是 inner join (取交集)

=>joins接收者可以是model类本身 也可以是 ActiveRecord::Relation 的实例

Province.joins(:cities)

SELECT `provinces`.* FROM `provinces` INNER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id`


Province.where(id: 1).joins(:cities)

SELECT `provinces`.* FROM `provinces` INNER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id` WHERE `provinces`.`id` = 1


上面分别用model类本身和ActiveRecord::Relation的实例调用了,

当然joins也支持多表来查:

Province.joins(:cities, :viliages)

Province.joins(:cities).joins(:viliages)

SELECT `provinces`.* FROM `provinces` INNER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id` INNER JOIN `viliages` ON `viliages`.`province_id` = `provinces`.`id`


其实上面的查询结果还存在一个问题,看第一个语句查询结果:

=> #<ActiveRecord::Relation [#<Province id: 1, name: "安徽", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01 00:00:00">, #<Province id: 1, name: "安徽", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01 00:00:00">, #<Province id: 1, name: "安徽", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01 00:00:00">, #<Province id: 2, name: "上海", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01 00:00:00">, #<Province id: 2, name: "上海", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01 00:00:00">]>


这个其实是inner join导致的安徽有三个城市,上海有两个经过inner join就变成这样了,那我们有咩有什么方式去重了,继续向下看:

Province.joins(:cities).uniq

SELECT DISTINCT `provinces`.* FROM `provinces` INNER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id`


=> #<ActiveRecord::Relation [#<Province id: 1, name: "安徽", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01 00:00:00">, #<Province id: 2, name: "上海", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01 00:00:00">]>


结果是我们想要的去重了,因为sql语句已经distinct了,但是在终端尝试的时候出现一个问题:

DEPRECATION WARNING: uniq is deprecated and will be removed from Rails 5.1 (use distinct instead) (called from irb_binding at (irb):17)


这句话大概告诉我们Rails 5.1后要废弃了,至于为什么废弃我们不关心,那么有没有其他方式了?

Province.joins(:cities).select(‘distinct provinces.*’)

SELECT distinct provinces.* FROM `provinces` INNER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id`


=> #<ActiveRecord::Relation [#<Province id: 1, name: "安徽", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01
4000
00:00:00">, #<Province id: 2, name: "上海", created_at: "2017-10-01 00:00:00", updated_at: "2017-10-01 00:00:00">]>


看到这个结果我们很开心实现这个效果了,当然我们从上面看好像都是在查province表的内容,那么我们能不能通过这个方式查cities表的内容了?

Province.joins(:cities).select(‘distinct cities.name’)

SELECT distinct cities.name FROM `provinces` INNER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id`


=> #<ActiveRecord::Relation [#<Province id: nil, name: "合肥">, #<Province id: nil, name: "庐江">, #<Province id: nil, name: "包头">, #<Province id: nil, name: "浦东">, #<Province id: nil, name: "浦西">]>


如果你想查cities表的内容后面指定下cities表中的字段就可以,你可以把这里的select当成我们查询sql语句的select就好理解了。

这里我还需要提醒一点的是,看下面代码:

province = Province.joins(cities)
province.each do |pro|
pro.cities
end


如果你去执行这里的pro.cities会向cities表继续查询,意思是joins避免不了n+1次查询,这点需要注意,不是预加载方式。

Preload:

=>preload其实就是分开查询

=>preload接收者可以是model类本身 也可以是 ActiveRecord::Relation 的实例

Province.preload(:cities)

SELECT `provinces`.* FROM `provinces` City
SELECT `cities`.* FROM `cities` WHERE `cities`.`province_id` IN (1, 2)


从上面的语句我们可以看出来,其实就是两次查询,在看下面语句

Province.where(id: 1).preload(:cities)

SELECT `provinces`.* FROM `provinces` WHERE `provinces`.`id` = 1
SELECT `cities`.* FROM `cities` WHERE `cities`.`province_id` = 1


看到这个结果我们就来思考个问题了,是不是preload总会生成两句sql语句,现在我们默认是,具体后面分析,看下面语句

Province.preload(:cities).where(cities: {province_id: 1})

SELECT `provinces`.* FROM `provinces` WHERE `cities`.`province_id` = 1


在测试的时候异常了,原因当然是province_id,这个字段provinces中并没有,这里分段查询,没有链表,使用preoload就不能这么干了,那么是不是preload不能指定where,当然不是只要指定是前面的查询表还是可以的,看一个例子吧。

Province.preload(:cities).where(id: 1)

SELECT `provinces`.* FROM `provinces` WHERE `provinces`.`id` = 1
SELECT `cities`.* FROM `cities` WHERE `cities`.`province_id` = 1


没有异常程序继续执行。

分析到这里我们已经知道了,preload其实就是分段查询,没有连接表,当然也是可以连接的,通过使用joins,但是连接之后还是分段查询的,看下面的例子

Province.joins(:cities).preload(:cities)

SELECT `provinces`.* FROM `provinces` INNER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id`
SELECT `cities`.* FROM `cities` WHERE `cities`.`province_id` IN (1, 2)


preload是分段查询,这里的分段数要看表的个数了,例如:

Province.preload(:cities,:viliages),这里就分了三段查询具体不实验了,其实preload是预加载的,这里不但把province的数据查出来,cities的数据也查出来直接保存到内存中了,以后使用就不需要查询了。

includes:

=>能分段查询就分段不行就链表查询

=>includes接收者可以是model类本身 也可以是 ActiveRecord::Relation 的实例

includes是老生常谈的东西了,实际开发中为了防止n+1次查询,都是通过includes解决的

Province.includes(:cities)

SELECT `provinces`.* FROM `provinces`
SELECT `cities`.* FROM `cities` WHERE `cities`.`province_id` IN (1, 2)


感觉跟preload的很像吧,分段查询,继续向下看

Province.includes(:cities).where(cities: {province_id: 1})

SELECT `provinces`.`id` AS t0_r0, `provinces`.`name` AS t0_r1, `provinces`.`created_at` AS t0_r2, `provinces`.`updated_at` AS t0_r3, `cities`.`id` AS t1_r0, `cities`.`name` AS t1_r1, `cities`.`province_id` AS t1_r2, `cities`.`created_at` AS t1_r3, `cities`.`updated_at` AS t1_r4 FROM `provinces` LEFT OUTER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id` WHERE `cities`.`province_id` = 1


从这里其实我么已经发现和preload区别了,如果是preload这里已经异常了,includes比较聪明,它发现自己分段查询查不了,那咋办了?这位大哥自主链表完成了查询,通过一句sql完成了,这里值得注意的是使用的是 LEFT OUTER JOIN,不是inner join。

那我们能不能强制让includes用一句sql来查询呢?

Province.includes(:cities).references(:cites)

SELECT `provinces`.`id` AS t0_r0, `provinces`.`name` AS t0_r1, `provinces`.`created_at` AS t0_r2, `provinces`.`updated_at` AS t0_r3, `cities`.`id` AS t1_r0, `cities`.`name` AS t1_r1, `cities`.`province_id` AS t1_r2, `cities`.`created_at` AS t1_r3, `cities`.`updated_at` AS t1_r4 FROM `provinces` LEFT OUTER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id`


看上面结果是可以的

到这里includes讲完了,其实跟preload的本质区别有以下两种

第一:includes如果能分段查询就分段查询,如果不能,会自动转为一句sql完成查询。

第二:references可以强制includes通过一句sql完成查询,preload不可以。

相同点: preload和includes都能预加载,一般使用includes多,原因是includes比preload聪明。

eager_laod:

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

=> 接受对象model或者是ActiveRecord::Relation的实例

Province.eager_load(:cities)

SELECT `provinces`.`id` AS t0_r0, `provinces`.`name` AS t0_r1, `provinces`.`created_at` AS t0_r2, `provinces`.`updated_at` AS t0_r3, `cities`.`id` AS t1_r0, `cities`.`name` AS t1_r1, `cities`.`province_id` AS t1_r2, `cities`.`created_at` AS t1_r3, `cities`.`updated_at` AS t1_r4 FROM `provinces` LEFT OUTER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id`


Province.where(id: 1).eager_load(:cities)

SELECT `provinces`.`id` AS t0_r0, `provinces`.`name` AS t0_r1, `provinces`.`created_at` AS t0_r2, `provinces`.`updated_at` AS t0_r3, `cities`.`id` AS t1_r0, `cities`.`name` AS t1_r1, `cities`.`province_id` AS t1_r2, `cities`.`created_at` AS t1_r3, `cities`.`updated_at` AS t1_r4 FROM `provinces` LEFT OUTER JOIN `cities` ON `cities`.`province_id` = `provinces`.`id` WHERE `provinces`.`id` = 1


其实我们发现了,这与 includes 在 where 指定了 cities 表的属性的情况下的单次查询完全相同,其实就是includes的其中的一个功能,如果指定了references,其实是一样的,当时也是预加载方式了。

总结: joins: 采用的是inner join,很容易查出重复数据需要去重。 preload:

总是分段查询,是预加载方式,想要链表,那就joins吧,其实还是要分段的,分段数根据表的个数判断。

includes:能分段就分段查询,不能就自动left join,同时组成一句sql语句,是预加载。 eager_load: left

join一句sql完成查询,于指定includes方式references用法相同,是预加载。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  activerecord class sql