停止在 Rails 中错误使用 .any?
I’m happy to translate the article for you, but I need the actual text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line exactly as you provided and preserve all formatting, markdown, and code blocks in the translation.
Source: …
介绍
一个传递给 .any? 的单块代码可能会悄悄地把成千上万条记录加载到内存中——没有警告,没有错误,只有不必要的对象。大多数 Rails 开发者都没有注意到这一点。
.any? 和 .exists? 都在回答同一个简单的问题:是否至少存在一条记录?
然而在内部,它们的行为可能大相径庭。本文将解释调用每个方法时会发生什么、何时使用哪一个,以及如何避免常见的性能陷阱。
基本用法 – 仅检查是否存在
如果你只需要知道关联中是否包含任何记录,.any? 和 .exists? 会生成相同的高效查询:
user.posts.any?
#=> SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1
user.posts.exists?
#=> SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1
不会把对象加载到内存中。两种方法都向数据库提出一个简单的是/否问题,并在找到第一条匹配记录后立即返回。
当你链式调用 .where 时同样适用——只要 不要 给 .any? 传入块:
user.posts.where(published: true).any?
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = true
# LIMIT 1
user.posts.where(published: true).exists?
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = true
# LIMIT 1
如果这就是你所需要的,随意选择在代码中更易读的那一个即可;两者在性能上没有差别。
Source:
.any? 带块使用
一旦你给 .any? 传入块,Rails 的行为就会完全改变。它不再向数据库查询,而是把 所有 匹配的记录加载到内存中,然后在 Ruby 中进行过滤:
user.posts.any? { |post| post.published? }
#=> SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
会发生什么
- 从数据库加载所有帖子。
- 为每一行实例化一个
ActiveRecord对象。 - 在 Ruby 中遍历它们。
- 返回一个布尔值。
在开发环境下这看起来无害,但在生产环境中可能会非常灾难性。如果一个用户有 50,000 条帖子,你刚刚为了检查其中是否有已发布的帖子而把 50,000 个对象全部加载进内存。
实现细节
def any?(*args)
return false if @none
return super if args.present? || block_given?
!empty?
end
当提供块时,Rails 会委托给 Enumerable#any?,而后者需要完整的集合在内存中。于是你实际上把过滤从 SQL 移到了 Ruby。
更倾向使用 SQL 进行过滤
把条件直接推到数据库:
user.posts.exists?(published: true)
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = TRUE
- 只返回一行、一列。
- 在找到第一条匹配记录后立即停止。
- 不会实例化对象。
得到相同的结果,却成本低得多。
当 .any? 实际是正确选择时
有一个重要的例外:如果关联已经 加载,.any? 不会再次访问数据库。
users = User.includes(:posts)
users.each do |user|
user.posts.any? { |post| post.published? } # no extra query
end
因为 posts 已经预加载,.any? 完全在内存中工作。在这种情况下:
.any?→ 没有额外查询。.exists?→ 强制执行新的 SQL 查询。
在这里使用 .exists? 可能会导致不必要的数据库调用,甚至产生 N+1 模式。Rails 会在内部检查关联是否已加载;.any? 的行为类似普通的 Ruby 集合。
指南摘要
| 关联状态 | 推荐方法 |
|---|---|
| 未加载 | exists?(或不带块的 any?) |
| 已加载 | any?(不带块) |
| 需要在 未加载 的关联上对块进行求值 | 避免 使用带块的 .any?;使用 SQL(带条件的 exists?) |
结论
小的 ActiveRecord API 差异可能对生产环境产生实际影响。在调用 .any? 之前,请自问:
- 我只是想检查是否存在吗?→ 使用
exists?(或不带块的any?)。 - 我即将加载整个集合吗?→ 避免在未加载的关联上使用带块的
.any?。
理解这些细微差别有助于编写更高效的 Rails 代码,防止隐藏的性能陷阱。