Migrations in Rails
· 5 min read · Updated March 29, 2026 · beginner
rails database activerecord migrations
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
```ruby
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:
```ruby
class AddSlugToArticles < ActiveRecord::Migration[7.0]
def change
add_column :articles, :slug, :string
add_index :articles, :slug, unique: true
end
end
```ruby
Removing a column works the same way:
```ruby
remove_column :articles, :slug
```ruby
You can rename columns if you misnamed something early on:
```ruby
rename_column :users, :login_count, :sign_in_count
```ruby
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:
```ruby
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
```ruby
## 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:
```ruby
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
```ruby
When you need conditional logic inside a reversible migration, use the `reversible` block:
```ruby
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
```ruby
## 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:
```ruby
create_table :comments do |t|
t.references :article, foreign_key: true, null: false
t.text :body
t.timestamps
end
```ruby
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:
```ruby
add_reference :comments, :commentable, polymorphic: true, index: true
```ruby
This adds both `commentable_type` and `commentable_id` columns.
The standalone `add_reference` method creates the same column outside a table creation block:
```ruby
add_reference :articles, :author, foreign_key: true, null: false
```ruby
If you already have a column and want to add the foreign key constraint separately, use `add_foreign_key`:
```ruby
add_foreign_key :articles, :authors, column: :author_id,
name: :fk_articles_author,
on_delete: :cascade
```ruby
## 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:
```bash
rails db:migrate:status
```bash
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`:
```bash
rails db:rollback STEP=3
```bash
To migrate to a specific version, pass the timestamp:
```bash
rails db:migrate VERSION=20260329100000
```bash
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`:
```ruby
10.times do |i|
Article.create!(
title: "Article #{i + 1}",
slug: "article-#{i + 1}",
published: i.even?
)
end
```ruby
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](/tutorials/rails-activerecord-basics/) — querying and saving records with ActiveRecord
- [Rails Routing](/tutorials/rails-routing/) — how requests reach your controllers
- [Getting Started with Rails](/tutorials/rails-getting-started/) — set up your first Rails application