mcpField
M
migrations

migrations

github.com/thinkoodleデータ・データベース7180 回インストール8 週間前に更新
リポジトリ

migrations について

Rails 8.1 の開発者向けに、安全で可逆な前進型のスキーマ変更を案内するスキルです。マイグレーションの生成、制約やインデックス、確認とロールバック、データおよび複数データベースの移行を扱います。

主な機能

  1. 01可逆なマイグレーションの作成
  2. 02マイグレーションによるスキーマ変更
  3. 03インデックス・外部キー・制約の追加
  4. 04状態とロールバックの確認
  5. 05スキーマ移行とデータ移行の分離
  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 を参照してください。

コメント