大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Active Record 模型
这一课你将能做到
- 写迁移文件建表、改表、加索引,并用
db:rollback安全回滚 - 给模型加验证(presence、uniqueness、自定义),让脏数据进不了数据库
- 声明关联(has_many/belongs_to),用模型回调在保存前后触发业务动作
为什么要学
模型是 Rails 应用的「心脏」。控制器和视图都可以很薄,但模型必须厚——业务规则、数据完整性、对象间关系全在这一层。Active Record 模式的核心思想是:一行数据库记录 = 一个 Ruby 对象,对象身上的方法就是你能对数据做的所有操作。
对比本站:Spring Boot 用 JPA/Hibernate,实体类里要写注解、getter/setter、Repository 接口;Rails 的模型类继承 ApplicationRecord 就什么都有了——属性读写、查询接口、验证、关联,零注解零模板代码。这是 Ruby 动态语言特性带来的红利,也是理解「约定优于配置」最好的样本。
先建立图像
模型与表的约定映射:
Ruby 世界 数据库世界
───────────── ─────────────
class Article 表 articles
title ←───────────────────── 列 title VARCHAR
body ←───────────────────── 列 body TEXT
save ─────────────────────► INSERT INTO ...
find(1) ────────────────────► SELECT * WHERE id=1
destroy ────────────────────► DELETE WHERE id=1
Article.has_many :comments ──► comments.article_id 外键
Comment.belongs_to :article ──► 同上,反向视角
一条记录的完整生命周期(回调挂载点):
new → validation → save → 数据库
│ │
before_validation before_save → before_create
after_validation after_create → after_save
跟着做
步骤 1:用迁移建表
bin/rails generate model Article title:string body:text published:boolean
生成的迁移文件 db/migrate/2026xxxxxxxx_create_articles.rb:
class CreateArticles < ActiveRecord::Migration[7.1]
def change
create_table :articles do |t|
t.string :title
t.text :body
t.boolean :published, default: false, null: false
t.timestamps # created_at + updated_at 两列
end
end
end
执行并验证:
bin/rails db:migrate
bin/rails console
Article.column_names # => ["id", "title", "body", "published", "created_at", "updated_at"]
步骤 2:改表与回滚
发现需要给标题加唯一索引:
bin/rails generate migration AddIndexToArticlesTitle
class AddIndexToArticlesTitle < ActiveRecord::Migration[7.1]
def change
add_index :articles, :title, unique: true
end
end
bin/rails db:migrate # 执行
bin/rails db:rollback # 后悔药:撤销最近一个迁移
bin/rails db:migrate:status # 查看所有迁移的执行状态
步骤 3:给模型加验证
app/models/article.rb:
class Article < ApplicationRecord
validates :title, presence: true, uniqueness: true, length: { minimum: 5 }
validates :body, presence: true, length: { minimum: 10 }
validate :published_must_have_body # 自定义验证
private
def published_must_have_body
if published? && body.blank?
errors.add(:body, "发布前必须填写正文")
end
end
end
在控制台体验验证拦截:
article = Article.new(title: "短")
article.save # => false,没写库!
article.errors.full_messages # => ["Title is too short (minimum is 5 characters)", "Body can't be blank", ...]
article.title = "一个合格的标题"
article.body = "这是一段足够长的正文内容"
article.save # => true
save 返回 false、不抛异常;save! 校验失败抛 ActiveRecord::RecordInvalid,后台任务里更常用。
步骤 4:声明关联
bin/rails generate model Comment commenter:string body:text article:references
bin/rails db:migrate
article:references 自动生成 article_id 外键列和索引。模型代码:
# app/models/article.rb
class Article < ApplicationRecord
has_many :comments, dependent: :destroy # 删文章时连带删评论
validates :title, presence: true
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :article # 每条评论必须有 article_id
validates :body, presence: true
end
使用:
article = Article.first
article.comments.create(commenter: "小明", body: "写得好")
article.comments.count # => 1
Comment.first.article.title # 反向导航
belongs_to 默认带必填验证(article_id 为空不合法),dependent: :destroy 保证不产生孤儿数据。
步骤 5:模型回调
文章发布时自动记录时间:
class Article < ApplicationRecord
before_save :set_published_at, if: :published?
private
def set_published_at
self.published_at ||= Time.current
end
end
回调适合「和数据生命周期强绑定」的动作;复杂业务流程(发邮件、调第三方接口)应放 Service 对象或后台任务,不要堆在回调里——回调链越长越难调试。
讲清楚要点
- 迁移是表结构的 Git:每个迁移文件是一次带时间戳的变更,
db:migrate按序执行,db:rollback逆序撤销。schema.rb只是执行结果的快照。 - 验证在 Ruby 层执行:
validates挡在 save 之前,失败时错误信息存进errors对象。数据库层的约束(唯一索引、NOT NULL)是最后防线,两层都要配——验证给友好错误提示,约束防并发写穿。 - 关联是双向声明:
has_many和belongs_to是一对镜像,外键永远放在belongs_to那一方的表里。 - 回调是双刃剑:
before_save、after_create这类钩子能自动触发逻辑,但隐式行为过多会让代码「动作不可见」,团队项目里要克制使用。 - N+1 预警:
article.comments第一次访问时才发 SQL(懒加载)。循环里访问关联会爆出 N+1 查询,解法是includes(:comments)预加载,下一课详谈。
你可能会懵的地方
-
change和up/down方法的区别?change用于可自动逆推的操作(create_table、add_column、add_index),rollback 时 Rails 自动生成反向操作。不可逆操作(如删列且无法推断类型)要写up(执行)和down(回滚)两个方法。 -
boolean 字段的
published?方法哪来的? Active Record 自动为 boolean 列生成?结尾的谓词方法、为所有列生成属性=赋值方法和「变化追踪」方法(如title_changed?)。 -
find和find_by区别?find(5)找不到时抛ActiveRecord::RecordNotFound(生产环境渲染 404);find_by(title: "x")找不到返回 nil。按 id 找且必须存在用 find,按条件找可能不存在用 find_by。 -
迁移已执行过,发现写错了能直接改文件重跑吗? 不能。已入库的迁移不会重跑。正确做法:
db:rollback撤销 → 改迁移文件 → 重新db:migrate。如果已推到生产,则写新迁移去修正,永不改已发布的迁移。 -
timestamps是什么魔法? 迁移里的t.timestamps生成created_at/updated_at两列,Active Record 在 create/update 时自动维护,无需手写。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 手改 schema.rb 或直连数据库改表 | 团队无法同步、无法回滚 | 写迁移文件 + db:migrate |
只有 validates :title, uniqueness: true 没加唯一索引 | 并发下两条相同记录同时通过验证写库 | 验证 + 数据库唯一索引双保险 |
has_many :comments 不加 dependent | 删文章留下孤儿评论 | dependent: :destroy |
| 回调里发邮件、调第三方 API | 保存变慢、失败难排查 | 放 after_commit + 后台任务 |
| 已执行的迁移文件直接手改 | 别人机器上不会重跑,结构不一致 | rollback 后改,或写新迁移修正 |
卡点急救
db:migrate报PG::DuplicateTable / table already exists:数据库里已有表但迁移记录缺失。检查db:migrate:status,必要时bin/rails db:drop db:create db:migrate重建开发库。ActiveRecord::RecordInvalid: Validation failed:在控制台用对象.errors.full_messages看具体哪条验证失败。- rollback 报
IrreversibleMigration:迁移用了不可逆操作且只写了change。补def down方法,或db:rollback STEP=0前先手动处理。
面试常问
-
Active Record 和 Data Mapper 两种 ORM 模式区别? 答:Active Record(Rails 采用):对象直接携带持久化方法,
user.save对象自己知道怎么存;领域模型与表结构一一对应,简单直接但耦合数据库。Data Mapper(如 Java 的 Hibernate 更偏此风格):领域对象是纯 POJO,持久化由独立的 Mapper/Repository 层负责,领域更纯粹但模板代码多。Rails 选 Active Record 是「让 80% 的 CRUD 场景零样板」的务实取舍。 -
模型验证和数据库约束的关系?为什么都要? 答:验证在应用层执行,能给出友好的用户错误信息,但有竞态(两个并发请求同时通过唯一性验证);数据库约束是唯一索引、NOT NULL 等物理防线,保证任何情况下数据合法。验证负责体验,约束负责正确性,缺一不可。
-
迁移为什么不能改已执行的历史文件? 答:迁移靠
schema_migrations表记录已执行的版本号,改历史文件不会触发重跑,会导致开发/测试/生产环境表结构漂移。正确做法是新增迁移修正,保证任何环境从头跑db:migrate都得到一致结构。
自测三问
t.timestamps生成了哪两列?谁在维护它们的值?- 唯一性验证为什么还要配数据库唯一索引?
has_many :comments, dependent: :destroy去掉dependent会发生什么?
小结一句话
模型层是业务规则的家:迁移管理表结构变迁,验证守住数据底线,关联表达对象关系,回调挂载生命周期动作。
延伸
- Active Record Basics(官方模型指南)
- 下一课:查询与优化——从 where 链到 N+1 歼灭战。