CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Ruby on Rails 实战入门/课时/Active Record 模型
RawBlame
课时目录12 节

Ruby on Rails 实战入门

  1. 01Rails 概览与项目结构
  2. 02路由设计
  3. 03控制器
  4. 04Active Record 模型
  5. 05查询与优化
  6. 06数据库适配
  7. 07视图与组件
  8. 08表单
  9. 09认证与授权
  10. 10资产与后台任务
  11. 11测试与调试
  12. 12部署与运维

Active Record 模型

验证、回调与迁移。

30 分钟

Active Record 模型

这一课你将能做到

  1. 写迁移文件建表、改表、加索引,并用 db:rollback 安全回滚
  2. 给模型加验证(presence、uniqueness、自定义),让脏数据进不了数据库
  3. 声明关联(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 对象或后台任务,不要堆在回调里——回调链越长越难调试。

讲清楚要点

  1. 迁移是表结构的 Git:每个迁移文件是一次带时间戳的变更,db:migrate 按序执行,db:rollback 逆序撤销。schema.rb 只是执行结果的快照。
  2. 验证在 Ruby 层执行:validates 挡在 save 之前,失败时错误信息存进 errors 对象。数据库层的约束(唯一索引、NOT NULL)是最后防线,两层都要配——验证给友好错误提示,约束防并发写穿。
  3. 关联是双向声明:has_many 和 belongs_to 是一对镜像,外键永远放在 belongs_to 那一方的表里。
  4. 回调是双刃剑:before_save、after_create 这类钩子能自动触发逻辑,但隐式行为过多会让代码「动作不可见」,团队项目里要克制使用。
  5. 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 后改,或写新迁移修正

卡点急救

  1. db:migrate 报 PG::DuplicateTable / table already exists:数据库里已有表但迁移记录缺失。检查 db:migrate:status,必要时 bin/rails db:drop db:create db:migrate 重建开发库。
  2. ActiveRecord::RecordInvalid: Validation failed:在控制台用 对象.errors.full_messages 看具体哪条验证失败。
  3. 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 都得到一致结构。

自测三问

  1. t.timestamps 生成了哪两列?谁在维护它们的值?
  2. 唯一性验证为什么还要配数据库唯一索引?
  3. has_many :comments, dependent: :destroy 去掉 dependent 会发生什么?

小结一句话

模型层是业务规则的家:迁移管理表结构变迁,验证守住数据底线,关联表达对象关系,回调挂载生命周期动作。

延伸

  • Active Record Basics(官方模型指南)
  • 下一课:查询与优化——从 where 链到 N+1 歼灭战。
加载本节练习…
← 上一课控制器下一课 →查询与优化