mcpField
M
migrations

migrations

github.com/thinkoodle数据与数据库7180 次安装8 周前更新
代码仓库

关于 migrations

面向 Rails 8.1 开发者的数据库迁移技能,指导安全、可逆且仅向前的 schema 变更。内容包括迁移生成、约束与索引、回滚验证、数据迁移及多数据库操作。

主要功能

  1. 01编写可逆迁移
  2. 02通过迁移管理 Schema 变更
  3. 03添加索引、外键和约束
  4. 04验证迁移状态与回滚
  5. 05区分 Schema 与数据迁移
  6. 06支持多数据库迁移

适用场景

  1. 01创建表或调整现有列
  2. 02为字段添加索引或外键
  3. 03规划可回滚的类型变更
  4. 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_reference
  • add_foreign_key / remove_foreign_key
  • add_timestamps / remove_timestamps
  • rename_column、rename_table、rename_index
  • change_column_default(需要 from: 和 to:)
  • change_column_null
  • add_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。

评论

支持 Markdown