Ruby on Rails Migrations — Schema Changes Data Types and Rollbacks Explained
In this tutorial, you will learn about Ruby on Rails Migrations. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby on Rails Migrations manage database schema changes through versioned Ruby DSL files, enabling safe creation of tables, columns, indexes, and rollbacks without writing raw SQL by hand.
What You'll Learn
- Creating and running migrations
- Adding, modifying, and removing columns
- Using data types and indexes
- Rolling back and managing schema versions
Why It Matters
Migrations make schema changes versioned, reversible, and team-friendly. Doda Browser uses Migration-like patterns for configuration schema changes. Durga Antivirus Pro uses migrations to manage its threat database schema across deployments. No more manual SQL scripts.
Real-World Use
Every Rails application uses migrations. Teams collaborate on schema changes through migration files. CI/CD pipelines run migrations during deployment. Production databases evolve safely through versioned migrations.
flowchart LR
A["Migrations"] --> B["Generate"]
B --> C["Create Table"]
C --> D["Modify"]
D --> E["Migrate"]
E --> F["Rollback"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Creating Migrations
# Create a new model (generates migration + model)
bin/rails generate model Article title:string body:text published:boolean
# Create standalone migration
bin/rails generate migration AddCategoryToArticles category:string
# Named migration with change detection
bin/rails generate migration AddPriceToProducts price:decimal
Migration File Structure
# db/migrate/20260628000001_create_articles.rb
class CreateArticles < ActiveRecord::Migration[7.2]
def change
create_table :articles do |t|
t.string :title
t.text :body
t.boolean :published, default: false
t.timestamps
end
end
end
Running Migrations
# Run pending migrations
bin/rails db:migrate
# Migrate to a specific version
bin/rails db:migrate VERSION=20260628000001
# Check migration status
bin/rails db:migrate:status
# Rollback last migration
bin/rails db:rollback
# Rollback last 3 migrations
bin/rails db:rollback STEP=3
# Redo last migration (rollback + migrate)
bin/rails db:migrate:redo
Creating and Modifying Tables
create_table
class CreateUsers < ActiveRecord::Migration[7.2]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.string :password_digest
t.integer :age
t.boolean :active, default: true
t.text :bio
t.decimal :balance, precision: 10, scale: 2
t.timestamps
end
end
end
Adding Columns
class AddDetailsToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :username, :string
add_column :users, :role, :integer, default: 0
add_column :users, :last_login_at, :datetime
add_column :users, :settings, :jsonb, default: {}
end
end
Modifying Columns
class ModifyUserColumns < ActiveRecord::Migration[7.2]
def change
# Change column type
change_column :users, :bio, :text
# Set default
change_column_default :users, :active, from: nil, to: true
# Rename column
rename_column :users, :password_digest, :password_hash
# Make nullable
change_column_null :users, :age, true
end
end
Removing Columns
class RemoveObsoleteColumns < ActiveRecord::Migration[7.2]
def change
remove_column :users, :legacy_field, :string
remove_columns :users, :old_field1, :old_field2
remove_timestamps :users
end
end
Data Types
create_table :examples do |t|
t.string :name # VARCHAR (default 255)
t.text :description # TEXT
t.integer :count # INTEGER
t.bigint :large_count # BIGINT
t.float :rating # FLOAT
t.decimal :price, precision: 10, scale: 2 # DECIMAL
t.boolean :active # BOOLEAN
t.date :birthday # DATE
t.time :opening_time # TIME
t.datetime :published_at # DATETIME/TIMESTAMP
t.timestamp :created_at # TIMESTAMP
t.binary :data # BLOB
t.json :metadata # JSON
t.jsonb :settings # JSONB (PostgreSQL)
t.references :user # Foreign key + index
t.belongs_to :category # Same as references
t.enum :status, enum_type: :article_status # Enum (PostgreSQL)
t.uuid :identifier # UUID
end
Indexes
class AddIndexesToUsers < ActiveRecord::Migration[7.2]
def change
# Basic index
add_index :users, :email, unique: true
# Composite index
add_index :users, [:last_name, :first_name]
# Index with condition
add_index :users, :email, where: "active = true"
# Index with order
add_index :users, :created_at, order: :desc
# Index type
add_index :users, :metadata, using: :gin # PostgreSQL
end
end
Foreign Keys
class AddForeignKeyToArticles < ActiveRecord::Migration[7.2]
def change
# Add with migration
add_reference :articles, :user, null: false, foreign_key: true
# Custom foreign key
add_foreign_key :articles, :users, column: :author_id
end
end
Reversible Migrations
For complex changes, define up and down methods:
class CombineNameFields < ActiveRecord::Migration[7.2]
def up
add_column :users, :full_name, :string
User.reset_column_information
User.find_each do |user|
user.update!(full_name: "#{user.first_name} #{user.last_name}")
end
remove_column :users, :first_name
remove_column :users, :last_name
end
def down
add_column :users, :first_name, :string
add_column :users, :last_name, :string
User.reset_column_information
User.find_each do |user|
names = user.full_name.split(" ", 2)
user.update!(first_name: names[0], last_name: names[1])
end
remove_column :users, :full_name
end
end
Using reversible block
class AddRoleToUsers < ActiveRecord::Migration[7.2]
def change
reversible do |dir|
dir.up do
execute "CREATE TYPE user_role AS ENUM ('admin', 'user', 'guest')"
end
dir.down do
execute "DROP TYPE user_role"
end
end
add_column :users, :role, :user_role, default: "user"
end
end
Seed Data
# db/seeds.rb
puts "Creating admin user..."
User.create!(
name: "Admin",
email: "admin@example.com",
password: "password123",
role: :admin
)
puts "Creating sample categories..."
["Ruby", "Rails", "Go", "Rust"].each do |name|
Category.find_or_create_by!(name: name)
end
puts "Seed complete!"
bin/rails db:seed
Data Migrations
For complex data changes, use a separate migration or rake task:
# lib/tasks/data_migrations.rake
namespace :data do
desc "Backfill slug for existing articles"
task backfill_slug: :environment do
Article.where(slug: nil).find_each do |article|
article.update!(slug: article.title.parameterize)
print "."
end
puts "\nDone!"
end
end
bin/rails data:backfill_slug
Common Mistakes
1. Editing Old Migrations
# Bad — editing a migration that's already been run
# Other developers get "already migrated" errors
# Good — create a new migration for the change
bin/rails generate migration AddIndexToUsersEmail
2. Irreversible Migrations
# Bad — can't roll back
def change
remove_column :users, :email
end
# Good — reversible
def change
remove_column :users, :email, :string
end
3. Not Testing Rollback
# Always test your rollback
bin/rails db:migrate
bin/rails db:rollback
bin/rails db:migrate # Should work cleanly
4. Adding Data in Schema Changes
# Bad — data migration mixed with schema
def change
add_column :users, :role, :string
User.update_all(role: "user") # Should be separate
end
# Good — separate schema and data changes
5. Running Migrations Without Backups
# In production, always backup first
pg_dump myapp_production > backup_$(date +%Y%m%d).sql
bin/rails db:migrate
Practice Questions
1. What is a migration in Rails?
A versioned Ruby file that describes a database schema change. Migrations are run in order to evolve the database schema alongside the application code.
2. How do you reverse a migration?
Use bin/rails db:rollback to reverse the last migration. Rails infers reverse operations for most changes (create_table → drop_table, add_column → remove_column).
3. What columns does t.timestamps add?
created_at and updated_at datetime columns. Rails automatically manages these — setting created_at on create and updating updated_at on every save.
4. When should you add an index?
Index columns used in WHERE clauses, JOIN conditions, ORDER BY, and uniqueness constraints. Common indexes: foreign keys, email, login dates, status fields.
Challenge: Write a migration that creates a reviews table with references to both a user and a product, a rating (1-5), a body text, and proper indexes and foreign keys.
Solution
class CreateReviews < ActiveRecord::Migration[7.2]
def change
create_table :reviews do |t|
t.references :user, null: false, foreign_key: true
t.references :product, null: false, foreign_key: true
t.integer :rating, null: false
t.text :body
t.boolean :verified_purchase, default: false
t.timestamps
end
add_index :reviews, [:user_id, :product_id], unique: true
add_index :reviews, :rating
add_check_constraint :reviews, "rating >= 1 AND rating <= 5", name: "rating_range"
end
end
FAQ
{{< faq question="Should I edit old migrations or create new ones?" >}} Never edit a migration that's been committed or run in production. Always create a new migration. Editing old migrations breaks the chain and causes conflicts for other developers. {{< /faq >}}
{{< faq question="What happens if a migration fails partway through?" >}}
The migration is rolled back if it's in a Transaction (default for most databases). Failed migrations are not recorded in the schema_migrations table, so they'll re-run on next db:migrate.
{{< /faq >}}
{{< faq question="How do I handle migrations in production?" >}}
Use bin/rails db:migrate in your deployment script. Consider using bin/rails db:migrate:status to check pending migrations first. Always backup before production migrations.
{{< /faq >}}
{{< faq question="What is the schema.rb file?" >}} db/schema.rb is the authoritative snapshot of your database structure. It's generated from migrations. Use it to view the current schema without running all migrations. Never edit it manually. {{< /faq >}}
{{< faq question="Can I use SQL in migrations?" >}}
Yes. Use execute("SQL statement") for database-specific operations. Use reversible blocks for SQL operations that need rollback logic. Prefer the Rails DSL when possible.
{{< /faq >}}
Try It Yourself
# Create a migration for a products table
bin/rails generate migration CreateProducts name:string price:decimal{10,2} description:text active:boolean
# Edit the generated migration to add indexes and defaults
# db/migrate/20260628000001_create_products.rb
class CreateProducts < ActiveRecord::Migration[7.2]
def change
create_table :products do |t|
t.string :name, null: false
t.decimal :price, precision: 10, scale: 2, null: false, default: 0.0
t.text :description
t.boolean :active, default: true
t.timestamps
end
add_index :products, :name
add_index :products, :active
end
end
bin/rails db:migrate
bin/rails db:rollback
bin/rails db:migrate
What's Next
Now that you understand migrations, learn about Active Record associations for defining relationships between models.
| Topic | Description | Link |
|---|---|---|
| Ruby Associations | belongs_to, has_many, has_one | {{< ref "30-associations" >}} |
| Ruby Active Record | ORM, queries, CRUD operations | {{< ref "26-active-record" >}} |
| Python Alembic | Compare Python's migration tool | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro