== Rails 中的三种数据预加载方式
在试用 rails 和 ActiveRecord 的时候,你可能已经非常熟练的试用 includes 来进行数据 的预加载了,但是你知道为什么有时候你会得到一个非常小非常漂亮的 sql,有时候却得到 一个非常大的 sql 吗?你知道 preload 和 eager_load 哪一个可以帮助你达到同样的效果吗? 你知道这些预加载的处理方式在 rails4 中有什么变化吗?如果你还不知道,那么今天这篇 文章来告诉你那些你不知道的预加载方式
== 数据准备
class User < ActiveRecord::Base
attr_accessible :email, :name
has_many :addresses, dependent: :destroy
end
class Address < ActiveRecord::Base
attr_accessible :city, :country, :postal_code, :street, :user_id
belongs_to :user
end
== db seed
rob = User.create!(name: "Robert", email: "[email protected]")
bob = User.create!(name: "Bobert", email: "[email protected]")
rob.addresses.create!(country: "poland", city: "wrocal", postal_code: "22", street: "Rynek")
rob.addresses.create!(country: "Paris", city: "wrocal2", postal_code: "2542", street: "8 rue chambiges")
bob.addresses.create!(country: "Bobert", city: "wroceal", postal_code: "22232", street: "tiergarten")
== rails3
当你要试用预加载这个特性的时候,通常会选择 includes 方法
不是用 includes 方法
users = User.all
users[0].addresses
users[1].addresses
SELECT "users".* FROM "users" SELECT "addresses".* FROM "addresses" WHERE "addresses"."user_id" = 1 SELECT "addresses".* FROM "addresses" WHERE "addresses"."user_id" = 2
使用 includes 方法,当查找某个 user 的地址时就不需要再做查询
users = User.includes(:addresses)
SELECT "users".* FROM "users" SELECT "addresses".* FROM "addresses" WHERE "addresses"."user_id" IN (1, 2)
users[0].addresses
users[1].addresses
那么另外两个方法是干什么用的呢?
users = User.preload(:addresses)
SELECT "users".* FROM "users" SELECT "addresses".* FROM "addresses" WHERE "addresses"."user_id" IN (1, 2) 可以看出来,preload 方法的执行效果和 includes 非常相似, 那么还有什么区别吗?继续往下看吧
现在来看看 eager_load
users = User.eager_laod(:addresses)
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."email" AS t0_r2, "users"."created_at" AS t0_r3, "users"."updated_at" AS t0_r4, "addresses"."id" AS t1_r0, "addresses"."user_id" AS t1_r1, "addresses"."country" AS t1_r2, "addresses"."street" AS t1_r3, "addresses"."postal_code" AS t1_r4, "addresses"."city" AS t1_r5, "addresses"."created_at" AS t1_r6, "addresses"."updated_at" AS t1_r7 FROM "users" LEFT OUTER JOIN "addresses" ON "addresses"."user_id" = "users"."id"
eager_load 与前两种方法完全不同,最神秘的是 rails 有两种方式来进行数据的预加载 我们会得出下面的结论
实际上,当我们要使用 preload 时,意味着我们要拆分查询语句 如果使用 eager_load ,则会生成一条 left join 查询语句, 那 includes 是干什么的呢?includes 则是 rails 用来动态选择两种 方式的一个方法,取决于查询条件,下面让我们看一下 includes 什么情况下会 代理 eager_load 生成一条查询语句
User.includes(:addresses).where("addresses.country = ?", "Poland")
User.eager_load(:addresses).where("addresses.country = ?", "Poland")
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."email" AS t0_r2, "users"."created_at" AS t0_r3, "users"."updated_at" AS t0_r4, "addresses"."id" AS t1_r0, "addresses"."user_id" AS t1_r1, "addresses"."country" AS t1_r2, "addresses"."street" AS t1_r3, "addresses"."postal_code" AS t1_r4, "addresses"."city" AS t1_r5, "addresses"."created_at" AS t1_r6, "addresses"."updated_at" AS t1_r7 FROM "users" LEFT OUTER JOIN "addresses" ON "addresses"."user_id" = "users"."id" WHERE (addresses.country = 'Poland')
那么当使用 preload 会发生什么情况呢?
User.preload(:addresses).where("addresses.country = ?", "poland")
SELECT "users".* FROM "users" WHERE (addresses.country = 'poland') SQLite3::SQLException: no such column: addresses.country rails 会抛出异常,因为 preload 没有进行 left join
你可能会想,这段代码的真正意图是什么?
ruby
User.includes(:addresses).where("addresses.country = ?", "poland")
你知道着段代码对应的是哪个目标吗?没错,第一个
那我们来看一下是否可以达到第二个和第三个==获取所有地址为 poland 的用户,并预加载这些用户关联的所有 addresses 首先获取地址为 poland 的用户
User.joins(:addresses).where("addresses.country = ?", "Poland")
然后去预加载着些用户所对应的 addresses, 使用 includes
r = User.joins(:addresses).
where("addresses.country = ?", "Poland").
includes(:addresses)
r[0].addresses
[ #
] 这个结果显然不是我们想要的,因为它只预加载了 rob 的一个地址,而我们要的 是 rob 所对应的所有地址对比一下输出结果,发现只不过是 join 的方式不同,第一个是 inner join, 第二个是 left join 但是对最终结果没有影响
r = User.joins(:addresses).
where("addresses.country = ?", "Poland").
includes(:addresses)
r = User.includes(:addresses).where("addresses.country = ?", "poland")
下面看一些 preload 的执行效果
u = User.joins(:addresses).
where("addresses.country = ?", "poland").
preload(:addresses)
u[0].addresses
SELECT "users".* FROM "users" INNER JOIN "addresses" ON "addresses"."user_id" = "users"."id" WHERE (addresses.country = 'Poland') SELECT "addresses".* FROM "addresses" WHERE "addresses"."user_id" IN (1)
[
, ] ] 这个才是我们想要的结果==获取所有用户,并预加载这些用户关联的所有 addresses 在关联关系中添加条件过滤
class User < ActiveRecord::Base
attr_accessible :email, :name
has_many :addresses, dependent: :destroy
has_many :polish_addresses, conditions: {country: "poland"},
class_name: "Address"
end
###preload & includes
r = User.preload(:polish_addresses)
r = User.includes(:polish_addresses)
SELECT "users".* FROM "users"
SELECT "addresses".* FROM "addresses" WHERE "addresses"."country" = 'Poland' AND "addresses"."user_id" IN (1, 2)
r[0].polish_addresses
[
#<Address id: 1, user_id: 1, country: "Poland", street: "Rynek", postal_code: "55-555", city: "Wrocław", created_at: "2013-12-08 11:26:50", updated_at: "2013-12-08 11:26:50">
]
r[1].polish_addresses
[]
###eager_load
r = User.eager_load(:polish_addresses)
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."email" AS t0_r2, "users"."created_at" AS t0_r3, "users"."updated_at" AS t0_r4,
"addresses"."id" AS t1_r0, "addresses"."user_id" AS t1_r1, "addresses"."country" AS t1_r2, "addresses"."street" AS t1_r3, "addresses"."postal_code" AS t1_r4, "addresses"."city" AS t1_r5, "addresses"."created_at" AS t1_r6, "addresses"."updated_at" AS t1_r7
FROM "users"
LEFT OUTER JOIN "addresses"
ON "addresses"."user_id" = "users"."id" AND "addresses"."country" = 'Poland'
r[0].polish_addresses
[
#<Address id: 1, user_id: 1, country: "Poland", street: "Rynek", postal_code: "55-555", city: "Wrocław", created_at: "2013-12-08 11:26:50", updated_at: "2013-12-08 11:26:50">
]
r[1].polish_addresses
[]
注意:预加载的是 polish_addresses,而不是 addresses
== Rails4
Rails4 现在鼓励使用新的 lambda 语法用于定义带条件的关联关系 因为在类加载的时候,关联关系只被加载了一次,新的语法用于解决这个问题
class User < ActiveRecord::Base
attr_accessible :email, :name
has_many :addresses, dependent: :destroy
has_many :polish_addresses, -> { where(country: 'poland')},
class_name: "Address"
end
像下面定义 scope 的时候,Time.now 应该在每次类加载的时候都重新获取系统时间 在开发环境下,它是自动重新加载的,所以在开发环境下不会发现这个问题,现在更倾向于 后面两种写法
# Bad
scope :from_the_past, where("happens_at <= ?", Time.now)
# OK
scope :from_the_past, -> { where("happens_at <= ?", Time.now) }
# OK
def self.from_the_past
where("happens_at <= ?", Time.now)
end
rails4 为关联关系和 scope 提供了一致的语法,防止我们的应用出现这样的 bug
不带条件的预加载和 rails3 效果是一样的
User.includes(:addresses)
User.preload(:addresses)
User.eager_load(:addresses)
稍微有点区别的是带有条件的预加载,在使用 includes 的时候,需要我们自己来 指定关联的表,rails 不再帮我们处理,通过 references(:addresses) 指定
User.includes(:addresses, :places).
where("addresses.country = ?", "Poland").
references(:addresses)
和 rails3 一样,也可以使用 eager_load 来做同样的事情,但是不需要用 references(:addresses) 指定关联表
User.eager_load(:addresses).where("addresses.country = ?", "Poland")
使用 preload 也和 rails3 一样,会抛出异常,找不到关联表
User.preload(:addresses).where("addresses.country = ?", "Poland")
在 rails4 中下面两种写法与 rails3 一样
User.joins(:addresses).
where("addresses.country = ?", "Poland").
preload(:addresses)
User.preload(:polish_addresses)
==总结 1.rails3 includes: 代理 preload 和 eager_load,取决过滤条件中是否出现关联表 preload: 用分解了的查询语句获取数据 eager_load: 通过 left join 合成一条查询语句进行加载 2.rails4 在 rails4 中,使用 includes 时,如果过滤条件中出现关联字段,则需要使用 references 来指定
微信:ruby 程序员 博文:http://michael-roshen.iteye.com/blog/2146511 原文:http://blog.arkency.com/2013/12/rails4-preloading/