RoR Database Migrations
Migrations are files that describe changes to your database structure. Each migration adds, removes, or modifies tables and columns. Rails runs migrations in order to build your database step by step, like following a recipe one instruction at a time.
Why Migrations Exist
Without migrations, every developer on your team would need to manually update their local database every time the schema changes. Migrations solve this by turning database changes into version-controlled code that anyone can run to get in sync.
Developer A adds a new column → writes a migration Developer B pulls the code → runs rails db:migrate Developer B's database → automatically has the new column
Creating a Migration
Rails generates migration files for you. Never create them by hand.
Create a new table: rails generate migration CreateProducts name:string price:decimal stock:integer Add a column: rails generate migration AddEmailToUsers email:string Remove a column: rails generate migration RemoveAgeFromUsers age:integer Add an index: rails generate migration AddIndexToUsersEmail
Each command creates a timestamped file in db/migrate/:
db/migrate/20240115120000_create_products.rb db/migrate/20240116083000_add_email_to_users.rb
The timestamp ensures Rails runs migrations in the correct order.
Inside a Migration File
db/migrate/20240115120000_create_products.rb
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name, null: false
t.decimal :price, precision: 8, scale: 2
t.integer :stock, default: 0
t.boolean :available, default: true
t.text :description
t.timestamps ← adds created_at and updated_at automatically
end
end
end
Column Data Types
| Type | Use For | Example |
|---|---|---|
| string | Short text (up to 255 chars) | name, email, title |
| text | Long text (unlimited) | body, description, notes |
| integer | Whole numbers | age, count, position |
| decimal | Numbers with decimals | price, weight, rate |
| boolean | True or false | active, published, verified |
| datetime | Date and time | published_at, expires_at |
| date | Date only | birthday, due_date |
| references | Foreign key to another table | user:references |
Running Migrations
Run all pending migrations: rails db:migrate Check which migrations have run: rails db:migrate:status Roll back the last migration: rails db:rollback Roll back the last 3 migrations: rails db:rollback STEP=3 Re-run a specific migration version: rails db:migrate:redo VERSION=20240115120000
Migration Status Output
rails db:migrate:status Status Migration ID Migration Name -------------------------------------------------- up 20240101000000 CreateUsers up 20240110000000 CreateArticles up 20240115120000 CreateProducts down 20240116083000 AddEmailToUsers ← not yet run
up means the migration has been applied. down means it has not been run yet.
Adding Columns to Existing Tables
rails generate migration AddPhoneToUsers phone:string
Generated migration:
class AddPhoneToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :phone, :string
end
end
Run rails db:migrate and the phone column appears in the users table immediately.
Adding Indexes
Indexes speed up database lookups on frequently searched columns.
class AddIndexToUsersEmail < ActiveRecord::Migration[7.1]
def change
add_index :users, :email, unique: true
end
end
unique: true also ensures no two users can have the same email address at the database level.
The Schema File
After every migration, Rails updates db/schema.rb to reflect the current database structure. This file is the single source of truth for your database layout.
db/schema.rb
ActiveRecord::Schema[7.1].define(version: 2024_01_16) do
create_table "users", force: :cascade do |t|
t.string "name", null: false
t.string "email", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
end
create_table "articles", force: :cascade do |t|
t.string "title"
t.text "body"
t.boolean "published", default: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
Never edit schema.rb by hand. Always use migrations to make changes so the history stays intact.
Migration Lifecycle Diagram
You need a new column
|
v
rails generate migration AddSlugToArticles slug:string
|
v
File created: db/migrate/20240120_add_slug_to_articles.rb
|
v
Edit the file if needed (add index, defaults, etc.)
|
v
rails db:migrate
|
v
Column added to articles table in database
|
v
db/schema.rb updated automatically
|
v
Commit both the migration file and schema.rb to Git
Always commit migration files to version control. They form the complete history of every database change your application has ever made.
