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