关于 migrations
面向 Rails 8.1 开发者的数据库迁移技能,指导安全、可逆且仅向前的 schema 变更。内容包括迁移生成、约束与索引、回滚验证、数据迁移及多数据库操作。
主要功能
- 01编写可逆迁移
- 02通过迁移管理 Schema 变更
- 03添加索引、外键和约束
- 04验证迁移状态与回滚
- 05区分 Schema 与数据迁移
- 06支持多数据库迁移
适用场景
- 01创建表或调整现有列
- 02为字段添加索引或外键
- 03规划可回滚的类型变更
- 04排查迁移失败或回滚问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill migrations安装后在 Claude Code 中输入/migrations确认已加载。
附带文件reference.md
详细介绍
Rails 迁移专家
为 Rails 8.1 应用编写安全、可逆且只向前推进的迁移。
黄金规则
以下规则不可妥协。违反任何一条都会给生产环境带来实际问题。
1. 绝不编辑旧迁移
迁移一旦提交(尤其是在生产环境中运行过),就永远冻结。不要修改它,而要编写新的迁移。
# BAD — editing an existing, committed migration
class CreateUsers < ActiveRecord::Migration[8.1]
def change
create_table :users do |t|
t.string :email
t.string :name # ← sneaking this in after the fact
end
end
end
# GOOD — new migration to add the column
class AddNameToUsers < ActiveRecord::Migration[8.1]
def change
add_column :users, :name, :string
end
end
原因: 其他开发者和环境已经运行过旧版本。编辑它会导致所有人的 db:migrate 出错,并造成架构漂移。
例外: 刚生成、尚未提交或在其他地方运行的迁移,可以随意编辑。
2. 绝不直接修改数据库
所有架构变更都必须通过迁移完成。没有例外。
# BAD
sqlite3 db/development.sqlite3 "ALTER TABLE users ADD COLUMN name TEXT"
# GOOD
bin/rails generate migration AddNameToUsers name:string
bin/rails db:migrate
原因: 直接修改不会被记录,schema.rb 会与数据库不同步,而且其他环境也不会有这些变更。
3. 始终向前推进
迁移只增不改。要修正错误,请编写新的迁移。
# Wrong column type? New migration.
class ChangePostsBodyToText < ActiveRecord::Migration[8.1]
def up
change_column :posts, :body, :text
end
def down
change_column :posts, :body, :string
end
end
# Need to remove a column? New migration.
class RemoveObsoleteColumnFromUsers < ActiveRecord::Migration[8.1]
def change
remove_column :users, :obsolete_field, :string
end
end
说明
第 1 步:了解要修改的内容
编写迁移前,先检查当前架构:
# Check current schema
cat db/schema.rb | grep -A 20 "create_table \"users\""
# Check migration status
bin/rails db:migrate:status
# Check existing migrations
ls -la db/migrate/
修改前先了解当前状态。
第 2 步:生成迁移
使用 Rails 生成器——它们会创建带有正确时间戳和类结构的文件。按照 Rails 约定,为迁移起描述性名称:
# Creating a table (CreateXxx)
bin/rails generate migration CreatePosts title:string body:text published_at:datetime
# Adding columns (AddXxxToYyy)
bin/rails generate migration AddCategoryToPosts category:string
# Removing columns (RemoveXxxFromYyy)
bin/rails generate migration RemoveLegacyFieldFromUsers legacy_field:string
# Adding a reference/foreign key
bin/rails generate migration AddUserRefToPosts user:references
# Adding an index
bin/rails generate migration AddPartNumberToProducts part_number:string:index
# General changes (descriptive name)
bin/rails generate migration ChangePostsBodyToText
生成器快捷写法:
name:string— 如果省略类型,默认类型为string,因此只写name也可以user:references— 创建user_id列、索引和外键price:decimal{5,2}— 精度和小数位数修饰符email:string!— 添加null: false约束
第 3 步:编写迁移主体
对于可逆操作,使用 change 方法(Rails 会自动处理回滚):
class CreatePosts < ActiveRecord::Migration[8.1]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :body
t.references :author, null: false, foreign_key: true
t.datetime :published_at
t.timestamps
end
add_index :posts, :published_at
add_index :posts, :title
end
end
对于不可逆操作,使用 up/down:
class ChangePostsExcerptToText < ActiveRecord::Migration[8.1]
def up
change_column :posts, :excerpt, :text
end
def down
change_column :posts, :excerpt, :string, limit: 500
end
end
对于混合操作(部分可逆,部分不可逆),在 change 中使用 reversible:
class AddStatusToPosts < ActiveRecord::Migration[8.1]
def change
add_column :posts, :status, :string, default: "draft"
reversible do |dir|
dir.up do
execute <<~SQL
UPDATE posts SET status = 'published' WHERE published_at IS NOT NULL
SQL
end
end
end
end
第 4 步:运行并验证
# Run pending migrations
bin/rails db:migrate
# Check it worked
bin/rails db:migrate:status
# If something's wrong with the LAST migration (before committing)
bin/rails db:rollback
# Fix the migration, then re-run
bin/rails db:migrate
第 5 步:提交 schema.rb
始终将 db/schema.rb(或 db/structure.sql)与迁移一并提交。该文件是权威的架构快照。
重要注意事项
始终为外键建立索引
每个外键列都需要索引。没有例外。
# BAD — foreign key without index
t.bigint :author_id, null: false
# GOOD — use references (index included automatically)
t.references :author, null: false, foreign_key: true
# GOOD — manual column with explicit index
t.bigint :author_id, null: false, index: true
为现有表添加 NOT NULL 列时必须设置默认值
如果表中已有记录,添加没有默认值的 NOT NULL 列将会失败。
# BAD — fails if table has data
add_column :posts, :status, :string, null: false
# GOOD — provide a default
add_column :posts, :status, :string, null: false, default: "draft"
绝不在迁移中引用应用模型
模型会随时间变化,而迁移一旦创建就会冻结。引用应用模型会导致迁移出错。
# BAD — model may not exist or may have different validations later
class BackfillPostSlugs < ActiveRecord::Migration[8.1]
def up
Post.find_each { |p| p.update!(slug: p.title.parameterize) }
end
end
# GOOD — use raw SQL
class BackfillPostSlugs < ActiveRecord::Migration[8.1]
def up
execute <<~SQL
UPDATE posts SET slug = lower(replace(title, ' ', '-')) WHERE slug IS NULL
SQL
end
end
# ACCEPTABLE — inline class that won't change
class BackfillPostSlugs < ActiveRecord::Migration[8.1]
class Post < ApplicationRecord; end
def up
Post.where(slug: nil).find_each do |post|
post.update_column(:slug, post.title.parameterize)
end
end
end
不要使用动态 Ruby 值作为默认值
# BAD — evaluates once at migration time, frozen forever
add_column :posts, :migrated_at, :datetime, default: Time.current
# GOOD — SQL function evaluates at row insertion time
add_column :posts, :migrated_at, :datetime, default: -> { "CURRENT_TIMESTAMP" }
调用 remove_column 时始终包含列类型
如果不指定类型,Rails 就无法还原迁移。
# BAD — irreversible
remove_column :users, :legacy_field
# GOOD — reversible
remove_column :users, :legacy_field, :string
change_column 不可逆
Rails 无法猜测旧类型。始终使用 up/down:
# BAD — will fail on rollback
def change
change_column :posts, :body, :text
end
# GOOD
def up
change_column :posts, :body, :text
end
def down
change_column :posts, :body, :string
end
可逆操作速查
在 change 中自动可逆:
create_table/drop_table(drop_table需要代码块和选项)add_column/remove_column(remove_column需要指定类型)add_index/remove_index(remove_index需要指定列和选项)add_reference/remove_referenceadd_foreign_key/remove_foreign_keyadd_timestamps/remove_timestampsrename_column、rename_table、rename_indexchange_column_default(需要from:和to:)change_column_nulladd_check_constraint/remove_check_constraint
需要使用 up/down 或 reversible:
change_column(类型变更)execute(原始 SQL)- 任何数据操作
数据迁移
小型、与架构相关的更新
使用 reversible 将其包含在迁移中:
class AddStatusToPosts < ActiveRecord::Migration[8.1]
def change
add_column :posts, :status, :integer, default: 0, null: false
reversible do |dir|
dir.up do
execute "UPDATE posts SET status = 1 WHERE published_at IS NOT NULL"
end
end
end
end
大型数据迁移
不要将它们放入架构迁移中。请使用 rake 任务或 maintenance_tasks gem:
# lib/tasks/data_migrations.rake
namespace :data do
desc "Backfill post statuses"
task backfill_post_statuses: :environment do
Post.where(status: nil).find_each(batch_size: 1000) do |post|
post.update_column(:status, post.published_at? ? 1 : 0)
end
end
end
为何分开处理? 数据迁移可能运行缓慢、可能需要在特定时间运行,而且其回滚特性与架构变更不同。
多数据库迁移
当应用使用多个数据库时:
# Default database migrations go in db/migrate/
# Other databases use their own directories (configured in database.yml)
# Run specific database migrations
bin/rails db:migrate # primary database
bin/rails db:migrate:animals # "animals" database (example)
# Generate for a specific database
bin/rails generate migration CreateDogs name:string --database animals
辅助数据库的迁移文件位于其配置的 migrations_paths 目录中(例如,db/animals_migrate/)。
命令参考
# Generate
bin/rails generate migration AddNameToUsers name:string
bin/rails generate model Post title:string body:text
# Run
bin/rails db:migrate # Run all pending
bin/rails db:migrate VERSION=20240428... # Migrate to specific version
# Rollback
bin/rails db:rollback # Undo last migration
bin/rails db:rollback STEP=3 # Undo last 3
bin/rails db:migrate:redo # Rollback + re-migrate last
# Status
bin/rails db:migrate:status # Show up/down status of all
# Schema
bin/rails db:schema:dump # Regenerate schema.rb
bin/rails db:schema:load # Load schema (destructive!)
# Setup / Reset
bin/rails db:setup # Create + load schema + seed
bin/rails db:prepare # Idempotent setup
bin/rails db:reset # Drop + setup
bin/rails db:migrate:reset # Drop + migrate from scratch
# Specific migration
bin/rails db:migrate:up VERSION=20240428...
bin/rails db:migrate:down VERSION=20240428...
# Environment
bin/rails db:migrate RAILS_ENV=test
何时使用此技能
- 创建新表或修改现有表
- 添加、删除或重命名列
- 添加索引或外键
- 更改列类型或约束
- 编写数据回填迁移
- 调试迁移错误或回滚问题
- 设置多数据库迁移
- 了解 schema.rb 与 structure.sql 的区别
有关详细的列类型、索引模式、UUID 设置、检查约束和高级示例,请参阅此技能目录中的 reference.md。