学习目标:掌握 ActiveRecord 的查询构建器、Arel 抽象层和查询优化技巧
引言#
ActiveRecord 提供了强大而优雅的查询 API,让你无需编写原生 SQL 就能完成复杂查询。本章我们将深入探讨查询接口的实现原理。
1. Relation - 查询构建器#
1.1 什么是 Relation?#
ActiveRecord::Relation
是查询的核心,它不是立即执行的:
# 这些都返回 Relation 对象,不执行 SQL
users = User.where(active: true)
users = users.where('age > ?', 18)
users = users.order(:name)
# 直到你真正需要数据时才执行
users.each { |u| puts u.name } # 此时才执行 SQL
Relation 的特点:
- 延迟执行(Lazy Loading):只在需要时才查询数据库
- 可链式调用(Chainable):可以不断添加查询条件
- 不可变(Immutable):每次调用返回新的 Relation 对象
users = User.all
# => #<ActiveRecord::Relation [...]>
users.object_id
# => 70123456789
active_users = users.where(active: true)
active_users.object_id
# => 70987654321 # 不同的对象!
# 原始 Relation 不变
users == User.all # => true (逻辑上)
1.2 Relation 的内部结构#
# activerecord/lib/active_record/relation.rb
module ActiveRecord
class Relation
# 多值方法(数组)
MULTI_VALUE_METHODS = [
:includes, # 预加载关联
:eager_load, # 急加载
:preload, # 预加载
:select, # 选择字段
:group, # 分组
:order, # 排序
:joins, # JOIN
:left_outer_joins,
:references,
:extending,
:unscope
]
# 单值方法(单个值)
SINGLE_VALUE_METHODS = [
:limit, # 限制数量
:offset, # 偏移量
:lock, # 锁
:readonly, # 只读
:distinct, # 去重
:create_with, # 创建时的默认属性
:skip_query_cache
]
# 子句方法
CLAUSE_METHODS = [
:where, # WHERE 子句
:having, # HAVING 子句
:from # FROM 子句
]
def initialize(model, table: nil, predicate_builder: nil, values: {})
@model = model
@table = table
@values = values # 存储所有查询条件
@loaded = false # 是否已加载数据
@records = nil # 查询结果
end
end
end
1.3 查询方法的实现#
# activerecord/lib/active_record/relation/query_methods.rb
module ActiveRecord
module QueryMethods
def where(opts = :chain, *rest)
if opts == :chain
WhereChain.new(spawn)
elsif opts.blank?
self
else
spawn.where!(opts, *rest) # 返回新的 Relation
end
end
def where!(opts, *rest)
# 修改当前 Relation
self.where_clause += build_where_clause(opts, rest)
self
end
def order(*args)
spawn.order!(*args)
end
def order!(*args)
# 添加排序条件
self.order_values += args
self
end
def limit(value)
spawn.limit!(value)
end
def limit!(value)
self.limit_value = value
self
end
end
end
关键方法:
spawn
:克隆当前 Relation,返回新对象where!
,order!
等:修改当前对象(带!
的方法)where
,order
等:返回新 Relation(不修改原对象)
2. 查询 DSL#
2.1 基础查询方法#
where - 条件查询#
# 哈希条件
User.where(name: 'John')
# SELECT * FROM users WHERE name = 'John'
# 多个条件(AND)
User.where(name: 'John', active: true)
# SELECT * FROM users WHERE name = 'John' AND active = 1
# 数组条件(IN)
User.where(id: [1, 2, 3])
# SELECT * FROM users WHERE id IN (1, 2, 3)
# 范围条件(BETWEEN)
User.where(age: 18..65)
# SELECT * FROM users WHERE age BETWEEN 18 AND 65
# 字符串条件
User.where("name = 'John'")
User.where("age > ?", 18)
User.where("name = :name AND age > :age", name: 'John', age: 18)
# NOT 条件
User.where.not(name: 'John')
# SELECT * FROM users WHERE name != 'John'
# LIKE 条件
User.where("name LIKE ?", "%John%")
order - 排序#
# 升序
User.order(:name)
User.order(name: :asc)
# SELECT * FROM users ORDER BY name ASC
# 降序
User.order(name: :desc)
# SELECT * FROM users ORDER BY name DESC
# 多字段排序
User.order(:name, created_at: :desc)
User.order("name ASC, created_at DESC")
# 反转排序
User.order(:name).reverse_order
# SELECT * FROM users ORDER BY name DESC
limit / offset - 分页#
# 限制数量
User.limit(10)
# SELECT * FROM users LIMIT 10
# 偏移量
User.limit(10).offset(20)
# SELECT * FROM users LIMIT 10 OFFSET 20
# 分页辅助方法
User.page(3).per(20) # 需要 kaminari 或 will_paginate gem
select - 选择字段#
# 选择特定字段
User.select(:id, :name)
# SELECT id, name FROM users
# 字符串形式
User.select("id, name, created_at")
# 聚合函数
User.select("COUNT(*) as user_count")
# 排除字段(Rails 7+)
User.select(User.column_names - ['password_digest'])
joins - 连接表#
# INNER JOIN
User.joins(:posts)
# SELECT users.* FROM users
# INNER JOIN posts ON posts.user_id = users.id
# 多表 JOIN
User.joins(:posts, :comments)
# 字符串形式
User.joins("LEFT JOIN posts ON posts.user_id = users.id")
# 嵌套关联
User.joins(posts: :comments)
# SELECT users.* FROM users
# INNER JOIN posts ON posts.user_id = users.id
# INNER JOIN comments ON comments.post_id = posts.id
# LEFT OUTER JOIN
User.left_outer_joins(:posts)
group / having - 分组和聚合#
# 分组
User.group(:role).count
# SELECT role, COUNT(*) FROM users GROUP BY role
# HAVING 子句
User.group(:role).having("COUNT(*) > 5")
# SELECT * FROM users GROUP BY role HAVING COUNT(*) > 5
# 复杂聚合
User.select("DATE(created_at) as date, COUNT(*) as count")
.group("DATE(created_at)")
.having("COUNT(*) > 10")
2.2 查找方法#
# 通过主键查找
User.find(1) # 找不到抛出异常
User.find_by(id: 1) # 找不到返回 nil
User.find_by!(id: 1) # 找不到抛出异常
# 查找多个
User.find(1, 2, 3) # 返回数组
User.find([1, 2, 3])
# 条件查找
User.find_by(email: 'john@example.com')
User.find_by(name: 'John', active: true)
# 第一个/最后一个
User.first # ORDER BY id ASC LIMIT 1
User.last # ORDER BY id DESC LIMIT 1
User.first(10) # 前 10 个
User.last(10) # 后 10 个
# 存在性检查
User.exists?(1) # => true/false
User.exists?(name: 'John')
User.where(active: true).exists?
# 查找或创建
User.find_or_create_by(email: 'john@example.com')
User.find_or_create_by!(email: 'john@example.com') # 验证失败抛出异常
User.find_or_initialize_by(email: 'john@example.com') # 不保存
2.3 作用域(Scopes)#
作用域是预定义的查询条件:
class User < ApplicationRecord
# 简单作用域
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
# 带参数的作用域
scope :created_after, ->(date) { where("created_at > ?", date) }
scope :with_role, ->(role) { where(role: role) }
# 链式作用域
scope :recent, -> { where("created_at > ?", 1.week.ago) }
scope :popular, -> { where("posts_count > 10") }
# 默认作用域(谨慎使用!)
default_scope { where(deleted_at: nil) }
end
# 使用作用域
User.active
User.active.recent
User.with_role('admin').popular
# 移除默认作用域
User.unscoped.all
类方法 vs 作用域:
class User < ApplicationRecord
# 作用域 - 总是返回 Relation
scope :active, -> { where(active: true) }
# 类方法 - 可以包含逻辑
def self.active
where(active: true)
end
# 类方法可以做更复杂的事情
def self.search(query)
return none if query.blank?
where("name LIKE ?", "%#{query}%")
end
end
3. Arel - 抽象关系代数#
3.1 什么是 Arel?#
Arel(A Relational Algebra)是 ActiveRecord 的 SQL 抽象层:
ActiveRecord DSL
↓
Arel
↓
SQL 字符串
为什么需要 Arel?
- 数据库无关的查询表示
- 支持复杂的查询构建
- 防止 SQL 注入
3.2 Arel 的使用#
# 获取 Arel Table
users = User.arel_table
# 构建条件
users[:name].eq('John')
# => Arel::Nodes::Equality
# 转换为 SQL
users[:name].eq('John').to_sql
# => "\"users\".\"name\" = 'John'"
# 在 ActiveRecord 中使用
User.where(users[:age].gt(18))
# SELECT * FROM users WHERE age > 18
# 复杂条件
User.where(
users[:age].gt(18).and(
users[:name].matches('%John%')
)
)
# SELECT * FROM users WHERE age > 18 AND name LIKE '%John%'
3.3 Arel 节点类型#
users = User.arel_table
# 比较操作
users[:age].eq(18) # =
users[:age].not_eq(18) # !=
users[:age].gt(18) # >
users[:age].gteq(18) # >=
users[:age].lt(18) # <
users[:age].lteq(18) # <=
# 范围
users[:age].in([18, 19, 20]) # IN
users[:age].not_in([18, 19, 20]) # NOT IN
users[:age].between(18..65) # BETWEEN
# 字符串匹配
users[:name].matches('%John%') # LIKE
users[:name].does_not_match('%John%') # NOT LIKE
# 逻辑操作
users[:age].gt(18).and(users[:active].eq(true)) # AND
users[:age].gt(18).or(users[:admin].eq(true)) # OR
# NULL 检查
users[:deleted_at].eq(nil) # IS NULL
users[:deleted_at].not_eq(nil) # IS NOT NULL
3.4 Arel 的高级用法#
# 子查询
sub_query = User.where(role: 'admin').select(:id).arel
Post.where(user_id: sub_query)
# CASE 语句
users = User.arel_table
case_node = Arel::Nodes::Case.new
.when(users[:role].eq('admin')).then(1)
.when(users[:role].eq('moderator')).then(2)
.else(3)
User.select(users[:name], case_node.as('priority'))
# 聚合函数
users[:id].count.as('user_count')
users[:age].average.as('avg_age')
users[:score].sum.as('total_score')
4. 查询缓存#
4.1 查询缓存机制#
Rails 自动缓存同一请求中的相同查询:
# 在一个请求中
User.find(1) # 执行 SQL
User.find(1) # 从缓存读取,不执行 SQL
# 但不同的查询不会缓存
User.where(id: 1).first # 执行 SQL(不同的查询)
查询缓存的生命周期:
请求开始
└─> 启用查询缓存
└─> 执行控制器 action
└─> 渲染视图
└─> 响应结束
└─> 清空查询缓存
4.2 手动控制缓存#
# 禁用查询缓存
User.uncached do
User.find(1) # 总是执行 SQL
end
# 启用查询缓存
User.cache do
User.find(1) # 执行 SQL
User.find(1) # 从缓存读取
end
# 清除缓存
ActiveRecord::Base.connection.clear_query_cache
4.3 缓存失效#
修改数据会使缓存失效:
User.cache do
u = User.find(1) # 执行 SQL
u.update(name: 'Jane')
User.find(1) # 再次执行 SQL(缓存已失效)
end
5. 预加载 - 解决 N+1 问题#
5.1 什么是 N+1 问题?#
# ❌ N+1 问题
posts = Post.limit(10)
posts.each do |post|
puts post.user.name # 每次循环执行一次 SQL!
end
# 执行了 11 次查询:
# 1. SELECT * FROM posts LIMIT 10
# 2. SELECT * FROM users WHERE id = 1
# 3. SELECT * FROM users WHERE id = 2
# ...
# 11. SELECT * FROM users WHERE id = 10
5.2 includes - 智能预加载#
# ✅ 使用 includes
posts = Post.includes(:user).limit(10)
posts.each do |post|
puts post.user.name # 不执行额外 SQL
end
# 只执行 2 次查询:
# 1. SELECT * FROM posts LIMIT 10
# 2. SELECT * FROM users WHERE id IN (1,2,3,...,10)
includes 的工作方式:
- 自动选择
preload
或eager_load
- 如果需要在 WHERE 中引用关联,使用
eager_load
- 否则使用
preload
5.3 preload - 分离查询#
# 使用两个独立查询
posts = Post.preload(:user).limit(10)
# 查询:
# 1. SELECT * FROM posts LIMIT 10
# 2. SELECT * FROM users WHERE id IN (...)
优点:
- 查询简单
- 可以使用缓存
缺点:
- 不能在 WHERE 中引用关联表
5.4 eager_load - JOIN 查询#
# 使用 LEFT OUTER JOIN
posts = Post.eager_load(:user).limit(10)
# 查询:
# SELECT posts.*, users.*
# FROM posts
# LEFT OUTER JOIN users ON users.id = posts.user_id
# LIMIT 10
优点:
- 一次查询获取所有数据
- 可以在 WHERE 中引用关联表
缺点:
- 查询复杂
- 大量数据时可能慢
5.5 多层预加载#
# 预加载嵌套关联
Post.includes(user: [:profile, :company])
Post.includes(:user, comments: :author)
# 复杂的预加载
Post.includes(:user, :comments)
.where(users: { active: true }) # 需要使用 eager_load
6. 计算方法#
6.1 聚合函数#
# count - 计数
User.count # SELECT COUNT(*) FROM users
User.where(active: true).count
User.count(:age) # 不计算 NULL
User.distinct.count(:age) # 去重计数
# average - 平均值
User.average(:age) # SELECT AVG(age) FROM users
# sum - 求和
Order.sum(:total) # SELECT SUM(total) FROM orders
# minimum / maximum
User.minimum(:age)
User.maximum(:age)
# 多个聚合
User.group(:role).count
# => {"admin" => 5, "user" => 100}
User.group(:role).average(:age)
# => {"admin" => 35.5, "user" => 28.3}
6.2 pluck 和 pick#
# pluck - 提取字段值
User.pluck(:email)
# => ["user1@example.com", "user2@example.com", ...]
# SELECT email FROM users
# 多个字段
User.pluck(:id, :email)
# => [[1, "user1@example.com"], [2, "user2@example.com"], ...]
# pick - 提取第一行的值
User.pick(:email)
# => "user1@example.com"
# SELECT email FROM users LIMIT 1
# pluck vs select
User.select(:email).map(&:email) # 返回 AR 对象,慢
User.pluck(:email) # 直接返回数组,快
6.3 批量查询#
# find_each - 批量迭代
User.find_each do |user|
# 默认每次加载 1000 条
user.do_something
end
# 自定义批量大小
User.find_each(batch_size: 500) do |user|
user.do_something
end
# find_in_batches - 批量处理
User.find_in_batches(batch_size: 1000) do |users|
# users 是一个数组,包含 1000 个用户
export_to_csv(users)
end
# in_batches - 返回 Relation
User.in_batches(of: 1000) do |relation|
relation.update_all(active: true)
end
为什么需要批量查询?
- 防止一次加载大量数据导致内存溢出
- 在处理大数据集时保持性能
7. 查询优化技巧#
7.1 使用 explain 分析查询#
# 查看执行计划
User.where(email: 'john@example.com').explain
# 输出类似:
# EXPLAIN SELECT `users`.* FROM `users` WHERE `users`.`email` = 'john@example.com'
# +----+-------------+-------+------+---------------+------+---------+------+------+-------------+
# | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
# +----+-------------+-------+------+---------------+------+--------+------+------+-------------+
# | 1 | SIMPLE | users | ALL | NULL | NULL | NULL | NULL | 1000 | Using where |
# +----+-------------+-------+------+---------------+------+--------+------+------+-------------+
7.2 索引优化#
# 添加索引的迁移
add_index :users, :email, unique: true
add_index :posts, :user_id
add_index :posts, [:user_id, :created_at]
# 检查索引使用
# 使用 explain 查看 key 列是否使用了索引
7.3 select 只选择需要的字段#
# ❌ 选择所有字段
User.all
# ✅ 只选择需要的字段
User.select(:id, :name, :email)
7.4 使用 exists? 代替 count#
# ❌ 慢
User.where(active: true).count > 0
# ✅ 快
User.where(active: true).exists?
7.5 避免在循环中查询#
# ❌ N+1 问题
user_ids.each do |id|
User.find(id).name
end
# ✅ 批量查询
users = User.where(id: user_ids).index_by(&:id)
user_ids.each do |id|
users[id].name
end
8. 高级查询技巧#
8.1 子查询#
# 使用子查询
subquery = Post.select(:user_id).where("views > 1000")
User.where(id: subquery)
# 使用 Arel
posts = Post.arel_table
users = User.arel_table
User.where(users[:id].in(posts.project(posts[:user_id]).where(posts[:views].gt(1000))))
8.2 UNION 查询#
# Rails 7+ 支持 UNION
active_users = User.where(active: true)
admin_users = User.where(role: 'admin')
active_or_admin = active_users.union(admin_users)
8.3 窗口函数(PostgreSQL)#
# 使用窗口函数
User.select(
"users.*, ROW_NUMBER() OVER (PARTITION BY role ORDER BY created_at) as row_num"
)
9. 实战示例#
9.1 复杂查询示例#
# 查找:
# - 活跃用户
# - 有超过 10 篇文章
# - 最近 30 天有评论
# - 按文章数排序
User.joins(:posts, :comments)
.where(active: true)
.where("comments.created_at > ?", 30.days.ago)
.group("users.id")
.having("COUNT(DISTINCT posts.id) > 10")
.order("COUNT(DISTINCT posts.id) DESC")
.distinct
9.2 性能对比#
require 'benchmark'
# 测试不同查询方式的性能
Benchmark.bm do |x|
x.report("N+1:") do
Post.limit(100).each { |p| p.user.name }
end
x.report("includes:") do
Post.includes(:user).limit(100).each { |p| p.user.name }
end
x.report("joins:") do
Post.joins(:user).select("posts.*, users.name").limit(100).each { |p| p.name }
end
end
10. 本章总结#
通过本章学习,你应该掌握:
- Relation 查询构建器:延迟执行、链式调用、不可变性
- 查询 DSL:where, order, joins, group 等方法
- Arel 抽象层:数据库无关的查询表示
- 查询缓存:自动缓存机制和手动控制
- 预加载:解决 N+1 问题的三种方式
- 查询优化:索引、explain、批量查询
11. 练习题#
- 使用 Arel 构建一个复杂的查询
- 找出你的项目中的 N+1 问题并修复
- 使用 explain 分析一个慢查询
- 实现一个自定义的查询作用域
12. 下一步#
在下一篇 ActiveRecord 关联关系 中,我们将学习:
- 关联的实现原理
- has_many, belongs_to 等宏
- 预加载策略
- 多态关联
返回:学习指南首页