1 Ruby 数据库生态概览
Ruby 拥有成熟的数据库工具链。与 PHP 的 PDO 统一底层不同,Ruby 生态更倾向于通过 ORM 层屏蔽数据库差异,同时也保留了直接操作数据库的能力。
| 工具 | 类型 | 说明 |
|---|---|---|
| ActiveRecord | ORM | Rails 默认 ORM,可独立使用,Active Record 模式 |
| Sequel | ORM / 查询构建器 | 轻量灵活,同时支持 Dataset API 和 Model 层 |
| sqlite3 | 驱动 | SQLite3 的 Ruby 绑定,直接执行 SQL |
| pg | 驱动 | PostgreSQL 的 Ruby 客户端 |
| mysql2 | 驱动 | MySQL/MariaDB 的高性能 Ruby 客户端 |
安装常用数据库 Gem
# SQLite3 驱动 + ActiveRecord
gem install activerecord sqlite3
# Sequel ORM
gem install sequel sqlite3
# PostgreSQL
gem install pg
# 或在 Gemfile 中声明
# gem 'activerecord', '~> 7.1'
# gem 'sqlite3', '~> 1.7'
2 SQLite3 直接操作
sqlite3 gem 提供了对 SQLite 数据库的直接访问,适合脚本、CLI 工具和轻量级应用。无需 ORM 的开销,直接写 SQL。
创建数据库与建表
require 'sqlite3'
db = SQLite3::Database.new('app.db')
db.results_as_hash = true
db.execute <<-SQL
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
SQL
插入数据
db.execute(
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
['张三', 'zhangsan@example.com', 28]
)
new_id = db.last_insert_row_id
puts "新用户 ID: #{new_id}"
users = [
['李四', 'lisi@example.com', 32],
['王五', 'wangwu@example.com', 25],
]
users.each do |name, email, age|
db.execute('INSERT INTO users (name, email, age) VALUES (?, ?, ?)', [name, email, age])
end
查询数据
user = db.get_first_row('SELECT * FROM users WHERE id = ?', [1])
puts "#{user['name']} - #{user['email']}" if user
db.execute('SELECT * FROM users WHERE age >= ? ORDER BY created_at DESC', [25]) do |row|
puts "#{row['name']} (#{row['age']}岁)"
end
count = db.get_first_value('SELECT COUNT(*) FROM users WHERE age >= ?', [25])
puts "符合条件: #{count} 人"
Prepared Statements
stmt = db.prepare('SELECT * FROM users WHERE name LIKE ? AND age > ?')
results = stmt.execute('%张%', 20)
results.each { |row| puts row['name'] }
stmt.close
stmt = db.prepare('INSERT INTO users (name, email, age) VALUES (?, ?, ?)')
[
['赵六', 'zhaoliu@example.com', 30],
['钱七', 'qianqi@example.com', 27],
].each { |data| stmt.execute(data) }
stmt.close
更新与删除
db.execute('UPDATE users SET name = ?, age = ? WHERE id = ?', ['张三丰', 29, 1])
puts "更新了 #{db.changes} 条记录"
db.execute('DELETE FROM users WHERE id = ?', [3])
puts "删除了 #{db.changes} 条记录"
⚡ sqlite3 gem 要点
results_as_hash = true— 让结果以 Hash 返回(默认是 Array)execute支持?占位符防止 SQL 注入get_first_row取单行,get_first_value取单个值- Prepared Statement 可复用,批量操作更高效
3 ActiveRecord(独立使用)
ActiveRecord 是 Rails 的核心组件,但完全可以脱离 Rails 独立使用。它实现了 Active Record 设计模式——每个模型类对应一张数据库表,每个实例对应一行记录。
建立连接
require 'active_record'
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: 'app.db'
)
ActiveRecord::Base.logger = Logger.new($stdout)
定义 Schema(迁移)
ActiveRecord::Schema.define do
create_table :users, force: false, if_not_exists: true do |t|
t.string :name, null: false
t.string :email, null: false
t.integer :age
t.timestamps
end
add_index :users, :email, unique: true, if_not_exists: true
create_table :posts, force: false, if_not_exists: true do |t|
t.references :user, foreign_key: true
t.string :title, null: false
t.text :body
t.timestamps
end
end
定义模型
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
validates :name, presence: true, length: { maximum: 100 }
validates :email, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :age, numericality: { greater_than: 0, allow_nil: true }
before_save :normalize_email
scope :adults, -> { where('age >= ?', 18) }
scope :recent, -> { order(created_at: :desc) }
private
def normalize_email
self.email = email.downcase.strip
end
end
class Post < ActiveRecord::Base
belongs_to :user
validates :title, presence: true
end
CRUD 操作
# CREATE
user = User.create!(name: '张三', email: 'zhangsan@example.com', age: 28)
user = User.new(name: '李四', email: 'lisi@example.com', age: 32)
user.save!
# READ
user = User.find(1)
user = User.find_by(email: 'zhangsan@example.com')
adults = User.adults.recent.limit(10)
adults.each { |u| puts "#{u.name} (#{u.age}岁)" }
User.where('age BETWEEN ? AND ?', 25, 35).order(:name).each do |u|
puts u.name
end
count = User.where('age >= ?', 25).count
# UPDATE
user = User.find(1)
user.update!(name: '张三丰', age: 29)
User.where('age < ?', 18).update_all(status: 'minor')
# DELETE
user.destroy
User.where('created_at < ?', 1.year.ago).destroy_all
关联查询
user = User.find(1)
user.posts.create!(title: '学习 Ruby', body: 'Ruby 是一门优雅的语言...')
user.posts.each { |post| puts post.title }
posts_with_users = Post.includes(:user).where('created_at > ?', 1.week.ago)
posts_with_users.each do |post|
puts "#{post.title} by #{post.user.name}"
end
⚡ ActiveRecord 核心概念
find按主键查找(不存在抛异常),find_by按条件查找(不存在返回 nil)save!/create!失败抛异常,save/create失败返回 falsescope定义可链式调用的查询片段,组合成复杂查询includes预加载关联,避免 N+1 查询问题
4 Sequel ORM
Sequel 是 Ruby 社区另一个流行的数据库工具,风格更接近 SQL 本身。它既是查询构建器,也是 ORM,你可以只用 Dataset API(不定义 Model),也可以两者结合。
连接与建表
require 'sequel'
DB = Sequel.sqlite('app.db')
DB.create_table? :users do
primary_key :id
String :name, null: false
String :email, null: false, unique: true
Integer :age
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
end
Dataset API(无需定义 Model)
users = DB[:users]
users.insert(name: '张三', email: 'zhangsan@example.com', age: 28)
all_users = users.all
puts all_users.inspect
adults = users.where { age >= 25 }.order(Sequel.desc(:created_at)).limit(10)
adults.each { |u| puts "#{u[:name]} (#{u[:age]}岁)" }
user = users.where(id: 1).first
puts user[:name] if user
users.where(id: 1).update(name: '张三丰', age: 29)
users.where(id: 3).delete
count = users.where { age >= 25 }.count
puts "成年用户: #{count}"
users.where(name: %w[张三 李四]).each { |u| puts u[:email] }
avg_age = users.avg(:age)
puts "平均年龄: #{avg_age}"
Sequel Model 用法
class User < Sequel::Model
one_to_many :posts
def validate
super
errors.add(:name, '不能为空') if name.nil? || name.empty?
errors.add(:email, '格式不正确') unless email&.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
dataset_module do
def adults
where { age >= 18 }
end
def recent
order(Sequel.desc(:created_at))
end
end
end
user = User.create(name: '王五', email: 'wangwu@example.com', age: 25)
user.update(age: 26)
User.adults.recent.limit(5).each { |u| puts u.name }
🔄 ActiveRecord vs Sequel 选型
- ActiveRecord:Rails 项目首选,社区最大,文档最全,Convention over Configuration 理念
- Sequel:更灵活,Dataset API 贴近 SQL 思维,多数据库连接管理更方便,适合非 Rails 项目
- 两者都支持 MySQL、PostgreSQL、SQLite 等主流数据库
5 事务处理
事务将多条数据库操作打包为原子单元——要么全部成功提交,要么全部回滚。Ruby 的两大 ORM 都提供了优雅的事务 API。
ActiveRecord 事务
ActiveRecord::Base.transaction do
from_account = Account.lock.find(from_id)
to_account = Account.lock.find(to_id)
raise '余额不足' if from_account.balance < amount
from_account.update!(balance: from_account.balance - amount)
to_account.update!(balance: to_account.balance + amount)
Transfer.create!(
from_account_id: from_id,
to_account_id: to_id,
amount: amount
)
end
在 transaction 块内抛出异常会自动回滚。ActiveRecord::Rollback 是特殊异常,触发回滚但不会向外传播:
ActiveRecord::Base.transaction do
user = User.create!(name: '测试用户', email: 'test@example.com')
if some_condition_fails?
raise ActiveRecord::Rollback
end
user.posts.create!(title: '第一篇')
end
User.transaction do
User.transaction(requires_new: true) do
User.create!(name: '内层操作', email: 'inner@example.com')
raise ActiveRecord::Rollback
end
User.create!(name: '外层操作', email: 'outer@example.com')
end
Sequel 事务
DB.transaction do
from_account = DB[:accounts].where(id: from_id).for_update.first
raise Sequel::Rollback if from_account[:balance] < amount
DB[:accounts].where(id: from_id).update(balance: Sequel[:balance] - amount)
DB[:accounts].where(id: to_id).update(balance: Sequel[:balance] + amount)
DB[:transfers].insert(
from_id: from_id,
to_id: to_id,
amount: amount,
created_at: Time.now
)
end
DB.transaction(savepoint: true) do
DB[:users].insert(name: '外层', email: 'outer@example.com')
DB.transaction(savepoint: true) do
DB[:users].insert(name: '内层', email: 'inner@example.com')
raise Sequel::Rollback
end
end
⚡ 事务要点
- ActiveRecord:块内抛异常自动回滚,
ActiveRecord::Rollback静默回滚 - Sequel:
Sequel::Rollback静默回滚,其他异常自动回滚并向外传播 - 悲观锁:ActiveRecord 用
.lock,Sequel 用.for_update - 嵌套事务:ActiveRecord 用
requires_new: true,Sequel 用savepoint: true
6 跨语言 ORM 对比
🔄 各语言数据库工具对比
| 特性 | Ruby ActiveRecord | PHP PDO | Python SQLAlchemy | Node Prisma |
|---|---|---|---|---|
| 模式 | Active Record | 原生 SQL | Data Mapper | Schema-first |
| 迁移 | 内置 Migration | 手动/Phinx | Alembic | prisma migrate |
| 查询语法 | User.where(age: 25) | prepare + execute | session.query(User) | prisma.user.findMany() |
| 验证 | 内置 validates | 需自行实现 | 需扩展 | Zod 等外部库 |
| 回调 | before/after_save | 无 | 事件监听 | middleware |
| 学习曲线 | 平缓 | 简单(纯 SQL) | 陡峭 | 平缓 |
各语言查找用户的写法对比:
# Ruby ActiveRecord
user = User.find_by(email: 'zhangsan@example.com')
users = User.where('age >= ?', 25).order(created_at: :desc).limit(10)
# PHP PDO
# $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
# $stmt->execute(['zhangsan@example.com']);
# Python SQLAlchemy
# user = session.query(User).filter_by(email='zhangsan@example.com').first()
# Node.js Prisma
# const user = await prisma.user.findUnique({ where: { email: 'zhangsan@example.com' } })
7 本章要点
🗄️ 数据库生态
sqlite3/pg/mysql2 驱动直接操作,ActiveRecord/Sequel ORM 层封装,按需选择
💎 ActiveRecord
Rails 标准 ORM,可独立使用;validates 验证、scope 链式查询、has_many 关联
🔗 Sequel
轻量灵活,Dataset API 贴近 SQL 思维,DB[:table].where(...) 直觉式查询
🔒 安全操作
ORM 自动参数化,原生驱动使用 ? 占位符,杜绝 SQL 注入
💰 事务管理
transaction { } 块内操作原子化,异常自动回滚,支持嵌套事务
🪶 SQLite
零配置嵌入式数据库,脚本和原型开发首选,三种方式(sqlite3/AR/Sequel)均支持
下一章预告:第五章将讲解 Ruby 的网络通信能力——Net::HTTP、Faraday HTTP 客户端、JSON 处理以及 Sinatra Web 框架入门。数据库 + 网络 = 完整的后端能力栈。