Skip to content

Ruby on Rails Migrations — Schema Changes Data Types and Rollbacks Explained

DodaTech Updated 2026-06-28 7 min read

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