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

TypeUse ForExample
stringShort text (up to 255 chars)name, email, title
textLong text (unlimited)body, description, notes
integerWhole numbersage, count, position
decimalNumbers with decimalsprice, weight, rate
booleanTrue or falseactive, published, verified
datetimeDate and timepublished_at, expires_at
dateDate onlybirthday, due_date
referencesForeign key to another tableuser: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.

Leave a Comment

Your email address will not be published. Required fields are marked *