Rails Migrations Deep — Schema Evolution and Data Migration
In this tutorial, you will learn about Rails Migrations Deep. We cover key concepts, practical examples, and best practices to help you master this topic.
Rails migrations manage database schema changes over time with reversible migrations, data migrations, indexes, foreign keys, and zero-downtime strategies for production deployments.
What You'll Learn
By the end of this tutorial, you'll write reversible migrations, create data migrations, add indexes and foreign keys, use change method helpers, and deploy migrations in production.
Why Migrations Matter
Migrations version the database schema alongside code. Every schema change is tracked, reversible, and consistently applied across all environments.
Real-World Use
A team adds a product ratings feature. They create a Migration for the ratings table, add foreign keys to products and users, backfill rating data, and deploy without downtime.
Migrations Path
flowchart LR
A[ActiveRecord] --> B[Migrations Deep]
B --> C[Reversible]
B --> D[Data Migrations]
B --> E[Indexes]
B --> F[Foreign Keys]
B --> G{You Are Here}
style G fill:#f90,color:#fff
Reversible Migrations
Use change method for automatic reversibility.
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, null: false
t.references :category, foreign_key: true
t.boolean :published, default: false
t.datetime :published_at
t.timestamps
end
add_index :products, :name
add_index :products, [:category_id, :published]
end
end
Irreversible Migrations
Handle non-reversible changes with up/down.
class RemoveUnusedColumns < ActiveRecord::Migration[7.1]
def up
remove_column :products, :legacy_field
remove_column :products, :deprecated_flag
end
def down
add_column :products, :legacy_field, :string
add_column :products, :deprecated_flag, :boolean, default: false
end
end
Data Migrations
Migrate data alongside schema changes.
class BackfillProductSlugs < ActiveRecord::Migration[7.1]
def up
Product.where(slug: nil).find_each do |product|
product.update!(slug: product.name.parameterize)
end
end
def down
# Data migration cannot be reversed
raise ActiveRecord::IrreversibleMigration
end
end
# Better approach for large tables - batch processing
class BackfillProductSlugs < ActiveRecord::Migration[7.1]
def up
Product.where(slug: nil).in_batches(of: 1000) do |batch|
batch.each do |product|
product.update_column(:slug, product.name.parameterize)
end
end
end
end
Indexes and Performance
Add indexes for query performance.
class AddIndexesForPerformance < ActiveRecord::Migration[7.1]
def change
# Composite index for common query
add_index :orders, [:user_id, :status, :created_at], name: "idx_orders_user_status_date"
# Partial index for filtered queries
add_index :products, :price, where: "published = true", name: "idx_products_published_price"
# Index with order
add_index :reviews, [:product_id, :created_at], order: { created_at: :desc }
# Concurrent index in production
disable_ddl_transaction!
add_index :users, :email, algorithm: :concurrently
end
end
Common Mistakes
1. Running Data Migrations in Schema Migration
Data migrations should be separate from schema migrations. Use a distinct migration.
2. Not Testing Rollbacks
Always test the down method. Irreversible migrations should raise explicitly.
3. Missing Indexes for Foreign Keys
Rails adds foreign key indexes automatically with references foreign_key: true.
4. Blocking Writes in Production
Adding a column with a default locks the table. Use add_column then backfill.
5. Not Using find_each for Large Tables
Loading all records at once causes memory issues. Use in_batches or find_each.
Practice Questions
1. What is the change method?
A migration method that Rails can reverse automatically for most operations.
2. How do you add a foreign key in a migration?
t.references :category, foreign_key: true or add_foreign_key :products, :categories.
3. What is a partial index?
An index that only includes rows matching a WHERE condition.
4. How do you safely run migrations in production?
Use algorithm: :concurrently for indexes, add columns without defaults, backfill separately.
5. Challenge: Write a migration that adds a column with a default and backfills.
class AddSlugToProducts < ActiveRecord::Migration[7.1]
def up
add_column :products, :slug, :string
add_index :products, :slug, unique: true
Product.where(slug: nil).find_each do |product|
product.update_column(:slug, product.name.parameterize)
end
change_column_null :products, :slug, false
end
def down
remove_column :products, :slug
end
end
FAQ
{{< faq "Can I use SQL in migrations?" "Yes. Use execute("SQL") for raw SQL operations." >}}Mini Project: Schema Evolution for a Blog
Create migrations for a blog's schema evolution.
class CreateBlogSchema < ActiveRecord::Migration[7.1]
def change
create_table :posts do |t|
t.string :title, null: false
t.string :slug, null: false
t.text :body
t.references :author, null: false, foreign_key: { to_table: :users }
t.string :status, default: "draft"
t.timestamps
end
add_index :posts, :slug, unique: true
add_index :posts, [:author_id, :status, :created_at], name: "idx_posts_author_status_date"
add_index :posts, :created_at, order: :desc
end
end
What's Next
Rails Validations Deep Rails Associations Deep Rails Forms Deep
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro