跳过正文

第5篇:ActiveRecord 查询接口

·1875 字·9 分钟
xieweicong
作者
xieweicong

学习目标:掌握 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 的特点

  1. 延迟执行(Lazy Loading):只在需要时才查询数据库
  2. 可链式调用(Chainable):可以不断添加查询条件
  3. 不可变(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 的工作方式

  • 自动选择 preloadeager_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. 本章总结
#

通过本章学习,你应该掌握:

  1. Relation 查询构建器:延迟执行、链式调用、不可变性
  2. 查询 DSL:where, order, joins, group 等方法
  3. Arel 抽象层:数据库无关的查询表示
  4. 查询缓存:自动缓存机制和手动控制
  5. 预加载:解决 N+1 问题的三种方式
  6. 查询优化:索引、explain、批量查询

11. 练习题
#

  1. 使用 Arel 构建一个复杂的查询
  2. 找出你的项目中的 N+1 问题并修复
  3. 使用 explain 分析一个慢查询
  4. 实现一个自定义的查询作用域

12. 下一步
#

在下一篇 ActiveRecord 关联关系 中,我们将学习:

  • 关联的实现原理
  • has_many, belongs_to 等宏
  • 预加载策略
  • 多态关联

上一篇ActiveRecord 基础架构

下一篇ActiveRecord 关联关系

返回学习指南首页