Migrations in Rails
Rails migrations give you a way to describe your database structure in Ruby code. Instead of writing raw SQL to create a table, you call a Ruby method. Rails translates that into the right SQL for whichever database you are using. Migration files live in db/migrate/ and are numbered in the order they run. That number is a timestamp, which keeps two developers from creating conflicting migration numbers.
Creating Tables
The most common migration you will write creates a table. The create_table method takes the table name and a block:
class CreateArticles < ActiveRecord::Migration[7.0]
def change
create_table :articles do |t|
t.string :title, null: false
t.text :body
t.integer :views, default: 0
t.boolean :published, default: false
t.timestamps
end
end
end
The t.timestamps line adds created_at and updated_at columns automatically. Rails manages those columns for you whenever you save a record.
You can skip the auto-incrementing primary key with id: false, or supply a custom primary key name. You can also pass if_not_exists: true to prevent Rails from raising an error if the table already exists when running in production or a shared development environment.
Adding and removing columns
Once a table exists, you will often need to add a column later. After launching your articles table, you might decide you need a slug column for URL-friendly article titles:
class AddSlugToArticles < ActiveRecord::Migration[7.0]
def change
add_column :articles, :slug, :string
add_index :articles, :slug, unique: true
end
end
Removing a column works the same way:
remove_column :articles, :slug
You can rename columns if you misnamed something early on:
rename_column :users, :login_count, :sign_in_count
Changing a column is possible but requires a bit more care. change_column works inside up/down methods rather than change, because Rails cannot always reverse it automatically:
class ChangeTitleLimit < ActiveRecord::Migration[7.0]
def up
change_column :articles, :title, :string, limit: 500
end
def down
change_column :articles, :title, :string, limit: 255
end
end
The change Method vs up and down
Most simple migrations use change. Rails inspects the operation and knows how to reverse it. add_column reverses to remove_column, create_table reverses to drop_table, and add_index reverses to remove_index.
Some operations are not reversible, however. If you delete rows as part of a migration, Rails cannot know how to restore them. In those cases, write up and down explicitly:
class RemoveLegacyData < ActiveRecord::Migration[7.0]
def up
execute "DELETE FROM articles WHERE legacy = 1"
end
def down
# You must restore the data manually if you ever roll back
execute "INSERT INTO articles (legacy) VALUES (1)"
end
end
When you need conditional logic inside a reversible migration, use the reversible block:
class ManageTrigger < ActiveRecord::Migration[7.0]
def change
reversible do |dir|
dir.up { execute "ENABLE TRIGGER audit_trigger" }
dir.down { execute "DISABLE TRIGGER audit_trigger" }
end
end
end
References and foreign keys
Rails offers two ways to create a foreign key relationship. The table builder methods t.references and t.belongs_to are equivalent and create a column named <association>_id with an index:
create_table :comments do |t|
t.references :article, foreign_key: true, null: false
t.text :body
t.timestamps
end
This produces an article_id column on comments with a foreign key constraint pointing back to articles. The polymorphic: true option is useful for comments that can belong to multiple models:
add_reference :comments, :commentable, polymorphic: true, index: true
This adds both commentable_type and commentable_id columns.
The standalone add_reference method creates the same column outside a table creation block:
add_reference :articles, :author, foreign_key: true, null: false
If you already have a column and want to add the foreign key constraint separately, use add_foreign_key:
add_foreign_key :articles, :authors, column: :author_id,
name: :fk_articles_author,
on_delete: :cascade
Running and rolling back migrations
The rake task rails db:migrate runs all pending migrations in order. To check what is running, rails db:migrate:status shows each migration with an up or down indicator:
rails db:migrate:status
If you made a mistake and need to undo the last migration, rails db:rollback drops the last batch. You can roll back more than one step with STEP=n:
rails db:rollback STEP=3
To migrate to a specific version, pass the timestamp:
rails db:migrate VERSION=20260329100000
If a migration fails partway through on PostgreSQL or MySQL, the entire migration rolls back automatically because those databases support transactional DDL. SQLite has limited support for transactional schema changes, so certain operations may leave partial results on failure.
Seeding Data
Migrations are for structure, not data. When you need initial data, use db/seeds.rb:
10.times do |i|
Article.create!(
title: "Article #{i + 1}",
slug: "article-#{i + 1}",
published: i.even?
)
end
Run it with rails db:seed. It is not part of the migration chain, so it does not run automatically when you run rails db:migrate.
Common Gotchas
Changing a column with change_column requires up/down methods. Rails will warn you if you try to use it inside change.
Column order matters in some databases. Adding a non-null column without a default to an existing table with rows will fail on most databases because every existing row needs a value. Add the column first with a default or nullable, populate the data, then tighten the constraint in a separate migration.
Avoid editing migrations that have already run on other machines. Instead, write a new migration to correct the schema. Shared migration history is a shared contract.
See Also
- ActiveRecord Basics — querying and saving records with ActiveRecord
- Rails Routing — how requests reach your controllers
- Getting Started with Rails — set up your first Rails application