Skip
Arish's avatar

22. Migrations


Database Migrations

Migrations are Ruby classes that make it easy to modify your database schema over time. They're like version control for your database.

Creating Migrations

With Model Generator

bash
1rails generate model Product name:string price:decimal description:text

Standalone Migration

bash
1rails generate migration AddCategoryToProducts category:string

Migration Naming Conventions

Rails infers actions from migration names:

bash
1# Add columns
2rails g migration AddFieldsToUsers age:integer bio:text
3# Generates: add_column :users, :age, :integer
4
5# Remove columns
6rails g migration RemoveAgeFromUsers age:integer
7# Generates: remove_column :users, :age
8
9# Create table
10rails g migration CreateProducts name:string
11# Generates: create_table :products
12
13# Add reference
14rails g migration AddUserRefToArticles user:references
15# Generates: add_reference :articles, :user

Migration Structure

ruby
1class CreateProducts < ActiveRecord::Migration[7.1]
2  def change
3    create_table :products do |t|
4      t.string :name
5      t.decimal :price, precision: 10, scale: 2
6      t.text :description
7      
8      t.timestamps
9    end
10  end
11end

Available Column Types

ruby
1create_table :examples do |t|
2  # Strings and text
3  t.string :title                    # VARCHAR(255)
4  t.string :code, limit: 10          # VARCHAR(10)
5  t.text :body                       # TEXT
6  
7  # Numbers
8  t.integer :count                   # INT
9  t.bigint :big_number              # BIGINT
10  t.float :score                     # FLOAT
11  t.decimal :price, precision: 10, scale: 2  # DECIMAL(10,2)
12  
13  # Boolean
14  t.boolean :active, default: true   # BOOLEAN
15  
16  # Date and time
17  t.date :birth_date                 # DATE
18  t.time :start_time                 # TIME
19  t.datetime :published_at           # DATETIME
20  t.timestamp :last_login            # TIMESTAMP
21  
22  # Binary and JSON
23  t.binary :data                     # BLOB
24  t.json :metadata                   # JSON
25  t.jsonb :settings                  # JSONB (PostgreSQL)
26  
27  # Special types
28  t.references :user, foreign_key: true  # user_id with FK
29  t.belongs_to :category             # Same as references
30  
31  t.timestamps                       # created_at and updated_at
32end

Common Migration Methods

Adding Columns

ruby
1class AddFieldsToUsers < ActiveRecord::Migration[7.1]
2  def change
3    add_column :users, :phone, :string
4    add_column :users, :age, :integer, default: 0
5    add_column :users, :verified, :boolean, default: false, null: false
6  end
7end

Removing Columns

ruby
1class RemoveAgeFromUsers < ActiveRecord::Migration[7.1]
2  def change
3    remove_column :users, :age, :integer
4  end
5end

Renaming Columns

ruby
1class RenameNameToFullName < ActiveRecord::Migration[7.1]
2  def change
3    rename_column :users, :name, :full_name
4  end
5end

Changing Column Types

ruby
1class ChangeDescriptionToText < ActiveRecord::Migration[7.1]
2  def change
3    change_column :products, :description, :text
4  end
5end
6
7# Reversible change
8class ChangeDescriptionToText < ActiveRecord::Migration[7.1]
9  def up
10    change_column :products, :description, :text
11  end
12  
13  def down
14    change_column :products, :description, :string
15  end
16end

Adding Indexes

ruby
1class AddIndexToUsers < ActiveRecord::Migration[7.1]
2  def change
3    # Simple index
4    add_index :users, :email
5    
6    # Unique index
7    add_index :users, :email, unique: true
8    
9    # Composite index
10    add_index :articles, [:user_id, :created_at]
11    
12    # Named index
13    add_index :users, :email, name: 'idx_users_email'
14    
15    # Conditional index (PostgreSQL)
16    add_index :users, :email, where: "active = true"
17  end
18end

Adding References

ruby
1class AddUserToArticles < ActiveRecord::Migration[7.1]
2  def change
3    add_reference :articles, :user, null: false, foreign_key: true
4  end
5end

Creating Join Tables

ruby
1class CreateArticlesTags < ActiveRecord::Migration[7.1]
2  def change
3    create_join_table :articles, :tags do |t|
4      t.index :article_id
5      t.index :tag_id
6    end
7  end
8end

Running Migrations

bash
1# Run all pending migrations
2rails db:migrate
3
4# Run specific migration
5rails db:migrate VERSION=20240115000000
6
7# Rollback last migration
8rails db:rollback
9
10# Rollback multiple migrations
11rails db:rollback STEP=3
12
13# Check migration status
14rails db:migrate:status
15
16# Redo last migration (rollback + migrate)
17rails db:migrate:redo
18
19# Reset database (drop, create, migrate)
20rails db:reset

Reversible Migrations

ruby
1class ChangeProductsPrice < ActiveRecord::Migration[7.1]
2  def change
3    # Rails can automatically reverse these:
4    add_column :products, :discount, :decimal
5    rename_column :products, :name, :title
6    add_index :products, :title
7  end
8end
9
10# For non-reversible changes, use up/down:
11class ConvertPriceToInteger < ActiveRecord::Migration[7.1]
12  def up
13    change_column :products, :price, :integer
14  end
15  
16  def down
17    change_column :products, :price, :decimal
18  end
19end
20
21# Or use reversible block:
22class AddConstraint < ActiveRecord::Migration[7.1]
23  def change
24    reversible do |dir|
25      dir.up do
26        execute "ALTER TABLE products ADD CONSTRAINT price_positive CHECK (price > 0)"
27      end
28      dir.down do
29        execute "ALTER TABLE products DROP CONSTRAINT price_positive"
30      end
31    end
32  end
33end

Data Migrations

Sometimes you need to migrate data too:

ruby
1class AddDefaultCategory < ActiveRecord::Migration[7.1]
2  def up
3    # Add column
4    add_column :products, :category, :string
5    
6    # Update existing records
7    Product.reset_column_information
8    Product.update_all(category: 'general')
9    
10    # Add constraint
11    change_column_null :products, :category, false
12  end
13  
14  def down
15    remove_column :products, :category
16  end
17end

Schema File

After migrations, Rails updates db/schema.rb:

ruby
1# db/schema.rb
2ActiveRecord::Schema[7.1].define(version: 2024_01_15_000000) do
3  create_table "users", force: :cascade do |t|
4    t.string "name"
5    t.string "email"
6    t.datetime "created_at", null: false
7    t.datetime "updated_at", null: false
8    t.index ["email"], name: "index_users_on_email", unique: true
9  end
10end

Load schema directly (faster than running all migrations):

bash
1rails db:schema:load

Best Practices

ruby
1# 1. Always use reversible migrations
2# Bad
3class AddStatus < ActiveRecord::Migration[7.1]
4  def change
5    execute "UPDATE users SET status = 'active'"  # Not reversible!
6  end
7end
8
9# Good
10class AddStatus < ActiveRecord::Migration[7.1]
11  def up
12    execute "UPDATE users SET status = 'active'"
13  end
14  def down
15    execute "UPDATE users SET status = NULL"
16  end
17end
18
19# 2. Use change_column_null for null constraints
20change_column_null :users, :email, false
21
22# 3. Add foreign key constraints
23add_foreign_key :articles, :users
24add_foreign_key :comments, :articles, on_delete: :cascade
25
26# 4. Add indexes for foreign keys and frequently queried columns
27add_index :articles, :user_id
28add_index :users, :email, unique: true

Migrations keep your database schema in sync across all environments!