Ruby on Rails Migrations Mastery
π Ruby on Rails Migrations Mastery: Build Databases the Right Way! π
βA great Rails application isnβt built by models aloneβitβs built on clean, reliable, and maintainable database migrations.β
When learning Ruby on Rails, many developers spend hours mastering Models, Controllers, and Views but ignore one of the most important componentsβthe Migration System.
Migrations are the backbone of every Rails application. They allow teams to change database structures safely, collaborate efficiently, deploy confidently, and maintain production databases without manually writing SQL every time.
Letβs master Rails Migrations from beginner to advanced level.
π What are Rails Migrations?
A Migration is a Ruby class that describes changes to your database schema.
Instead of manually writing SQL like:
ALTER TABLE users ADD COLUMN age INTEGER;
Rails allows you to write:
class AddAgeToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :age, :integer
end
end
Rails converts this into SQL automatically for PostgreSQL, MySQL, SQLite, MariaDB, etc.
Think of migrations as version control for your database, just like Git is version control for your code.
π€ Why Do We Need Migrations?
Imagine working on a team.
Developer A adds:
users.name
Developer B adds:
users.phone
Developer C adds:
users.address
Without migrations:
- β Everyone edits SQL manually
- β Production database becomes inconsistent
- β Different environments have different schemas
- β Impossible to track database history
With migrations:
001_create_users
002_add_phone_to_users
003_add_address_to_users
Everyone simply runs:
rails db:migrate
Everything stays synchronised.
π How Rails Migrations Work
Every migration has a version timestamp.
Example:
20260726101010_create_products.rb
Inside:
class CreateProducts < ActiveRecord::Migration[8.0]
def change
create_table :products do |t|
t.string :name
t.decimal :price
t.timestamps
end
end
end
When running:
rails db:migrate
Rails checks:
schema_migrations
table.
It stores:
20260726101010
If already present:
β Skip
Otherwise:
β Execute migration.
π§© Anatomy of a Migration
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.integer :age
t.timestamps
end
end
end
Here,
create_table
creates table.
t.string
creates VARCHAR.
t.timestamps
creates:
created_at
updated_at
automatically.
π Migration Lifecycle
Generate Migration
β
βΌ
Edit Migration
β
βΌ
rails db:migrate
β
βΌ
Database Updated
β
βΌ
schema.rb Updated
β
βΌ
Version Saved
π Creating Migrations
Generate model:
rails g model Product name:string price:decimal stock:integer
Only migration:
rails g migration AddGSTToProducts gst:decimal
Remove column:
rails g migration RemoveGSTFromProducts gst:decimal
Rename:
rails g migration RenameStockToQuantity
π Common Migration Methods
Create Table
create_table :books do |t|
t.string :title
t.integer :pages
end
Add Column
add_column :users, :phone, :string
Remove Column
remove_column :users, :phone
Rename Column
rename_column :users, :fullname, :name
Rename Table
rename_table :clients, :customers
Change Column Type
change_column :products, :price, :decimal
Add Index
add_index :users, :email
Unique:
add_index :users, :email, unique: true
Composite:
add_index :orders,
[:user_id, :status]
Remove Index
remove_index :users, :email
Foreign Keys
add_reference :orders,
:customer,
foreign_key: true
Equivalent:
t.references :customer,
foreign_key: true
Add Boolean
add_column :users,
:verified,
:boolean,
default: false
π change vs up/down
change
Automatically reversible.
def change
add_column :users,
:phone,
:string
end
up/down
Use for complex operations.
def up
execute "UPDATE users SET active=true"
end
def down
execute "UPDATE users SET active=false"
end
π Data Migrations
Sometimes you need to update existing data.
Example:
User.update_all(active: true)
Better:
User.find_each do |user|
user.update(active: true)
end
Even better for very large datasets:
User.in_batches.update_all(active: true)
π§ Schema.rb
Rails automatically updates:
db/schema.rb
Never edit it manually.
It represents your current database structure.
π Structure.sql
Use when:
- PostgreSQL functions
- Triggers
- Views
- Advanced indexes
- Database extensions
Enable:
config.active_record.schema_format = :sql
β‘ Rollback
Undo last migration:
rails db:rollback
Rollback multiple:
rails db:rollback STEP=5
Specific version:
rails db:migrate VERSION=20260726101010
π₯ Reset Database
Drop database:
rails db:drop
Create:
rails db:create
Load schema:
rails db:schema:load
Reset everything:
rails db:reset
π§± Transactions
Rails wraps migrations inside database transactions.
If something fails:
Everything rolls back.
Safer deployments.
π Reversible Migrations
reversible do |dir|
dir.up do
execute "..."
end
dir.down do
execute "..."
end
end
π Performance Tips
Add Indexes
add_index :orders, :user_id
Without index:
10 million rows
β
Slow search
With index:
Milliseconds
Use Concurrent Indexes (PostgreSQL)
disable_ddl_transaction!
add_index :users,
:email,
algorithm: :concurrently
Prevents long table locks during deployment.
Avoid Loading Models
Bad:
User.all.each
Good:
execute <<~SQL
UPDATE users
SET active=true
SQL
or
User.in_batches.update_all(active: true)
π‘ Advanced Migration Hacks
Conditional Column
unless column_exists?(:users, :phone)
add_column :users,
:phone,
:string
end
Conditional Table
unless table_exists?(:logs)
create_table :logs do |t|
t.string :name
end
end
Reversible SQL
reversible do |dir|
dir.up do
execute "CREATE VIEW ..."
end
dir.down do
execute "DROP VIEW ..."
end
end
Execute Raw SQL
execute <<~SQL
UPDATE products
SET stock = 0
WHERE stock IS NULL;
SQL
Bulk Changes
change_table :users,
bulk: true do |t|
t.string :city
t.string :country
end
π Best Practices
β One Purpose Per Migration
Good:
AddPhoneToUsers
Bad:
UpdateEverythingMigration
β Small Migrations
One change
One commit
One migration
β Always Add Indexes
Especially:
Foreign Keys
Search Fields
Unique Columns
β Name Clearly
Good:
AddGSTToProducts
Bad:
Migration1
β Test Rollback
Always verify:
rails db:migrate
rails db:rollback
β Keep Migrations Deterministic
Avoid depending on external APIs, current time (unless intentional), or application logic that may change.
β Use Database Constraints
Prefer:
add_index :users, :email, unique: true
change_column_null :users, :email, false
over relying only on model validations.
β Common Mistakes
π« Editing old migrations after production deployment.
π« Forgetting indexes.
π« Huge data updates in one transaction.
π« Loading every record into memory.
π« Removing important columns without backup.
π« Using changing application models inside old migrations.
π« Not testing rollbacks.
π« Skipping foreign keys.
π« Making multiple unrelated schema changes in one migration.
π« Running long blocking operations during peak traffic.
π Production Migration Checklist
β Backup database
β Review migration code
β Add indexes where required
β Test locally
β Test on staging
β Check rollback
β Estimate runtime
β Use concurrent indexes when supported
β Monitor logs during deployment
β Verify schema after migration
π― Real-World Example
Suppose youβre building an e-commerce platform.
Step 1
rails g model Product \
name:string \
price:decimal \
stock:integer
Step 2
Run:
rails db:migrate
Step 3
Later, add SKU and barcode:
rails g migration AddSkuAndBarcodeToProducts \
sku:string \
barcode:string
Step 4
Optimise lookups:
add_index :products, :sku, unique: true
add_index :products, :barcode
Step 5
Enforce data integrity:
change_column_null :products, :sku, false
The database evolves incrementally without disrupting existing data or requiring manual SQL.
π Final Thoughts
Rails Migrations are much more than a convenienceβthey are the foundation of safe, collaborative, and scalable database evolution. Treat each migration as a permanent piece of your applicationβs history. Keep them focused, reversible where possible, backed by constraints and indexes, and tested before deployment.
When your migrations are clean, your deployments become smoother, your teammates stay in sync, and your production database remains reliable as your application grows.
π¬ βCode changes your application. Migrations change its history. Write both with care.β
Happy Rails coding! πβ¨
© Lakhveer Singh Rajput - Blogs. All Rights Reserved.