Skip to Content
Course content

86: Rails Migrations In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this happen constantly with developers moving from a "prototype" mindset to a "production" mindset: they treat migration files like they treat Ruby classes. They think, "I made a mistake in the column name in that migration from yesterday; I'll just open the file, fix the typo, and run rails db:migrate again."

Here is why that is a dangerous mistake. Let's say you ran a migration that created a posts table with a column called auther_name (a typo). You ran bin/rails db:migrate, and the table was created in your local database. You then notice the typo, change the code in the migration file to author_name, and run the migrate command again. Rails will tell you Nothing to do.

It doesn't care that you changed the text in the file. Rails tracks which migrations have already run by storing their timestamps in a special internal table called schema_migrations. Once a timestamp is in that table, Rails ignores that file forever. You've now created a "drift" where your code says one thing, but your actual database says another. If a teammate pulls your code, they'll get the "fixed" version; but your production server is still stuck with auther_name. You've just broken your deployment pipeline.

Migrations are a Ledger, Not a Blueprint

The most important shift in your mental model needs to be this: Migration files are a journal of changes, not a definition of the current state.

If you need to change a column name, you don't edit the migration that created the column. You create a new migration that alters the existing column. This preserves the history of the database. When a new developer joins the project, they run all migrations in order, recreating the exact evolution of the database schema that you experienced.

# Don't edit the old file. Create a new one:
# bin/rails generate migration RenameAutherNameToAuthorInPosts

class RenameAutherNameToAuthorInPosts < ActiveRecord::Migration[7.0]
  def change
    rename_column :posts, :auther_name, :author_name
  end
end

This is why we have db/schema.rb. That file is the blueprint. It is the authoritative source of what your database looks like right now. You should almost never edit schema.rb manually; it is automatically updated by Rails every time you run a migration.

When the change Method Isn't Enough

For most things—adding columns, creating tables—the change method is magic. Rails looks at the command and knows exactly how to reverse it. If you add_column in change, Rails knows that rollback means remove_column.

But Rails isn't omniscient. Some operations are "irreversible." Take remove_column. If you tell Rails to remove :email from the users table, and then you decide to rollback that migration, Rails has no idea what the data type of that column was or what options (like null: false) were set. It can't just "guess" how to put it back.

In these cases, you have to ditch change and use the explicit up and down methods. I personally prefer this for any complex data migration because it forces me to think about the "undo" path before I push to production.

class RemoveEmailFromUsers < ActiveRecord::Migration[7.0]
  def up
    remove_column :users, :email
  end

  def down
    # We explicitly define how to rebuild the column if we rollback
    add_column :users, :email, :string, null: false
  end
end

If you use change for a destructive action without providing the necessary information for a rollback, you'll hit a nasty ActiveRecord::IrreversibleMigration error when you try to go back. In a production emergency, that's the last thing you want to see.




📋 Practical Task

Exercise: Implementing a Reversible Unique Constraint on User Profiles

You are working on a project where the user_profiles table needs a unique constraint on the username column to prevent duplicates. However, the team is worried about the migration failing if there are already duplicate usernames in the development database, and they want to ensure the migration is fully reversible.

Your Goal: Create a migration that adds a unique index to the username column of the user_profiles table.

  • Constraint: Do not use the change method. Use up and down to ensure the index is explicitly created and removed.
  • Requirement: The index should be named unique_username_index.
  • Validation: After writing the migration, describe (in a comment or a separate text file) what would happen if you tried to run this migration on a database that already contains two users with the username "rails_pro".

Starter Schema:

create_table "user_profiles" do |t|
  t.string "username"
  t.text "bio"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end
Rating
0 0

There are no comments for now.

to be the first to leave a comment.