ActiveRecord Migrations
Comprehensive guide to creating and managing database migrations in Rails.
Creating Migrations
Generator Commands
bash
# Standalone migration rails generate migration AddPartNumberToProducts part_number:string # Create table rails generate migration CreateProducts name:string price:decimal # Add reference/foreign key rails generate migration AddUserRefToProducts user:references # Add index rails generate migration AddIndexToUsersEmail
Naming Conventions
Migration names determine auto-generated code:
| Pattern | Generated Code |
|---|---|
AddColumnToTable | add_column :table, :column, :type |
RemoveColumnFromTable | remove_column :table, :column, :type |
CreateTable | create_table block |
AddIndexToTable | add_index :table, :column |
File format: YYYYMMDDHHMMSS_migration_name.rb
Column Types
| Type | Description |
|---|---|
:string | Short text (VARCHAR, default 255) |
:text | Long text (unlimited) |
:integer | Standard integer |
:bigint | Large integer (default for PKs in Rails 5.1+) |
:float | Floating point |
:decimal | Precise decimal (use for money) |
:boolean | True/false |
:date | Date only |
:time | Time only |
:datetime | Date and time |
:timestamp | Same as datetime |
:binary | Binary data |
:json | JSON (native if supported) |
:jsonb | Binary JSON (PostgreSQL) |
:uuid | UUID type |
:references | Foreign key column |
Column Modifiers
ruby
add_column :products, :name, :string, null: false, # NOT NULL constraint default: "Untitled", # Default value limit: 100, # Max length (string) or bytes (integer) precision: 8, # Total digits (decimal) scale: 2, # Digits after decimal index: true, # Create index unique: true, # Unique index comment: "Product name" # Database comment
Table Operations
Create Table
ruby
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2
t.integer :quantity, default: 0
t.boolean :active, default: true
t.references :category, foreign_key: true
t.references :user, null: false, foreign_key: true
t.timestamps # created_at and updated_at
end
add_index :products, :name
add_index :products, [:category_id, :active]
end
end
Modify Table
ruby
class AddFieldsToProducts < ActiveRecord::Migration[7.1]
def change
change_table :products do |t|
t.string :sku
t.remove :temporary_field
t.rename :old_name, :new_name
t.index :sku, unique: true
end
end
end
Drop Table
ruby
def change
drop_table :products do |t|
# Include schema for reversibility
t.string :name
t.timestamps
end
end
Column Operations
Add Columns
ruby
def change add_column :products, :weight, :decimal, precision: 8, scale: 2 add_column :products, :dimensions, :json add_column :users, :role, :string, default: "member", null: false end
Remove Columns
ruby
def change # Include type for reversibility remove_column :products, :discontinued, :boolean, default: false end
Rename Column
ruby
def change rename_column :products, :upc, :sku end
Change Column
ruby
# Not automatically reversible - use up/down def up change_column :products, :price, :decimal, precision: 12, scale: 2 change_column_null :products, :name, false change_column_default :products, :status, from: nil, to: "draft" end def down change_column :products, :price, :decimal, precision: 10, scale: 2 change_column_null :products, :name, true change_column_default :products, :status, from: "draft", to: nil end
References and Foreign Keys
Add Reference
ruby
def change # Adds category_id column with index and foreign key add_reference :products, :category, foreign_key: true # Polymorphic reference add_reference :comments, :commentable, polymorphic: true, index: true # Without foreign key constraint add_reference :products, :supplier, foreign_key: false # Nullable reference add_reference :products, :brand, null: true, foreign_key: true end
Foreign Key Options
ruby
def change add_foreign_key :articles, :authors # Custom column name add_foreign_key :articles, :users, column: :author_id # On delete behavior add_foreign_key :comments, :articles, on_delete: :cascade add_foreign_key :orders, :users, on_delete: :nullify # Remove foreign key remove_foreign_key :articles, :authors remove_foreign_key :articles, column: :author_id end
Indexes
ruby
def change # Basic index add_index :users, :email # Unique index add_index :users, :email, unique: true # Composite index add_index :products, [:category_id, :status] # Partial index (PostgreSQL) add_index :orders, :shipped_at, where: "shipped_at IS NOT NULL" # Named index add_index :products, :sku, name: "idx_products_sku" # Remove index remove_index :users, :email remove_index :products, name: "idx_products_sku" end
The change Method
Auto-reversible operations (use change):
- •
add_column/remove_column(with type) - •
add_foreign_key/remove_foreign_key - •
add_index/remove_index - •
add_reference/remove_reference - •
add_timestamps/remove_timestamps - •
create_table/drop_table(with block) - •
create_join_table/drop_join_table - •
rename_column - •
rename_index - •
rename_table - •
change_column_default(with:fromand:to) - •
change_column_null
Reversible and Up/Down
Using reversible
ruby
def change
create_table :products do |t|
t.string :name
t.timestamps
end
reversible do |dir|
dir.up do
execute <<-SQL
CREATE INDEX idx_products_search ON products
USING gin(to_tsvector('english', name));
SQL
end
dir.down do
execute "DROP INDEX idx_products_search;"
end
end
end
Using up/down
ruby
def up change_column :products, :price, :decimal, precision: 12, scale: 2 end def down change_column :products, :price, :decimal, precision: 10, scale: 2 end
Irreversible Migration
ruby
def up drop_table :legacy_products end def down raise ActiveRecord::IrreversibleMigration end
Running Migrations
bash
# Run pending migrations rails db:migrate # Migrate to specific version rails db:migrate VERSION=20240101120000 # Rollback last migration rails db:rollback # Rollback multiple migrations rails db:rollback STEP=3 # Redo last migration (rollback + migrate) rails db:migrate:redo # Check migration status rails db:migrate:status # Run in specific environment rails db:migrate RAILS_ENV=production
Database Commands
bash
# Create database rails db:create # Drop database rails db:drop # Reset (drop + create + migrate) rails db:reset # Setup (create + load schema + seed) rails db:setup # Prepare (idempotent setup) rails db:prepare # Load schema directly (faster than migrations) rails db:schema:load # Dump current schema rails db:schema:dump # Run seeds rails db:seed
Safe Migration Practices
Do's
- •Always include column type in remove_column for reversibility
- •Use
change_column_defaultwith:fromand:to - •Add indexes on foreign keys (automatic with
references) - •Test rollback:
rails db:migrate:redo - •Keep migrations small and focused
Don'ts
- •Don't edit committed migrations - create new ones
- •Don't use model classes directly in migrations (schema may change)
- •Don't add data migrations - use seeds or maintenance tasks
Data Migration Alternative
ruby
# Instead of data in migrations, use:
# lib/tasks/data_migration.rake
namespace :data do
desc "Migrate user status values"
task migrate_status: :environment do
User.where(status: "old_value").update_all(status: "new_value")
end
end
Concurrent Index (PostgreSQL)
ruby
class AddIndexToUsersEmail < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :users, :email, algorithm: :concurrently
end
end
Additional Resources
Reference Files
- •
references/migration-patterns.md- Complex migration patterns, zero-downtime migrations