停止杀死你的数据库:5 个 ActiveRecord 提升 Rails 应用速度的技巧
Source: Dev.to
在本文中,我分享了自己使用 ActiveRecord 的历程。刚开始接触 Rails 时,我以为 ActiveRecord 只是把数据“魔法”般保存到数据库的工具。随着参与更大的项目,我意识到它的功能远不止这些。写出能正常工作的代码很容易,但写出让应用变慢的代码也同样容易。下面列出了五个层次的 ActiveRecord 知识,从简单查询到更高级的优化。
LEVEL 1: The Basics (Chaining)
大多数初学者只知道 find 和 where。ActiveRecord 的酷点在于,你可以无限链式调用这些方法,直到真正请求数据时才会执行查询。
# Instead of doing this
users = User.where(active: true)
recent_users = users.where('created_at > ?', 1.day.ago)
# You can chain it nicely
User.where(active: true).where('created_at > ?', 1.day.ago)
这只会生成 一条 SQL 查询。Rails 会等到最后一刻才去触碰数据库。
LEVEL 2: Cleaning up with Scopes
在多个控制器里重复相同的 where 条件非常烦人。可以在模型中定义 scopes;它们的行为类似类方法。
# app/models/post.rb
class Post { where(published: true, deleted: false) }
scope :recent, -> { where('created_at > ?', 1.week.ago) }
end
现在控制器代码就干净多了:
# app/controllers/posts_controller.rb
def index
@posts = Post.live.recent
end
LEVEL 3: Solving the N+1 Problem (Advanced)
最常见的错误是先加载父记录,然后在循环中遍历其子记录,导致每个关联都产生一次额外查询。
# controller
@posts = Post.all
# view (ERB)
Rails 会为帖子执行 1 条查询,再为每篇帖子的用户执行 N 条额外查询(每篇帖子一次)。如果有 100 篇帖子,就会产生 101 条查询,严重拖慢性能。
STEP 1: Use includes
告诉 ActiveRecord 在一次查询中把关联记录一起取回。
@posts = Post.includes(:user).all
Rails 现在只会执行 2 条 查询:
- 获取所有帖子。
- 获取这些帖子关联的所有用户。
LEVEL 4: Pluck vs Map (Optimization)
当只需要单列数据时,避免加载完整的 ActiveRecord 对象。
# Heavy: loads all User objects into memory
User.all.map(&:email)
改用 pluck 直接从数据库取出列:
# Fast: fetches only the email column
User.pluck(:email)
pluck 返回字符串数组,不会实例化模型对象,从而节省内存和时间。
LEVEL 5: Use exists? instead of present?
如果只需要判断记录是否存在,别去加载整条记录。
# Bad: loads the post into memory just to check existence
if Post.where(id: params[:id]).present?
# …
end
# Good: runs a lightweight SQL `SELECT 1` with LIMIT 1
if Post.exists?(params[:id])
# …
end
exists? 返回布尔值,并在找到匹配项后立即停止查询。
掌握这些小技巧后,你的 Rails 应用会更快,代码也会更简洁。