Migrations change your database schema in a controlled, repeatable way. This lesson covers how migrations work, how to write complex ones, and how to deal with existing data.
Important
Work on this lesson in
advisormode.
Learning goals
- You can explain what migrations are for, and why schema changes go through migrations instead of manual changes on a server.
- You can write migrations that change the schema and migrate existing data.
- You can explain what
db/schema.rbis for: Rails regenerates it after each migration, and new databases (a fresh clone, CI, the test database) load it instead of replaying all migrations. - You can explain why we don't use application models in migrations, and what to use instead (e.g. plain SQL or a model class defined inside the migration).
- You can explain how migrations interact with feature branches, and handle switching branches with pending or rolled-back migrations.
- You can keep a cached column up to date with callbacks and backfill it for existing records.
- You can read complex migrations in a real project and explain what they do.
Resources
Read what's new to you, skim what's familiar, skip what you already master. Stop when you can meet the learning goals.
Your agent can also generate an overview, a tutorial or an explanation for anything here, tailored to what you already know. Just ask.
- 📄 Rails Guide: Active Record Migrations Show archive.org snapshot — the reference; read it fully once
- 📄 How to use models in your migrations (without killing kittens) — why we never use application models in migrations (archived blog post)
- 📄 How to write complex migrations in Rails — our card
- 📄 Rails Guide: Active Record Callbacks Show archive.org snapshot — for keeping the cached columns of the exercise up to date
The schema file
Rails regenerates db/schema.rb after every migration, and you commit it together with the migration. It describes the current state of the whole database. Don't edit it by hand: write a migration and let Rails regenerate the file.
Because it describes the whole database, the schema file is the fastest way to build a new one. A fresh clone, the test database or a CI run loads it in one step with rails db:schema:load (or rails db:prepare) instead of replaying every migration since the project started. That is faster, and it still works when old migrations no longer run because the code they used has changed or is gone.
Some projects use db/structure.sql instead, a plain SQL dump, when the schema uses database features that Rails' Ruby DSL can't express. It plays the same role.
The schema file is also a quick overview of an app's data model, as you saw in SQL basics.
Exercises
Read real migrations
Check out the repository Project Hero and read through some migrations in db/migrate. Find two complex migrations and discuss them with your mentor.
Tip
Find the largest files in the
db/migratefolder.
Cached columns
In MovieDB, add two fields:
-
Actor#total_movies: This field should cache the total number of movies an actor stars in -
Actor#first_movie_id: This field should cache the first movie to which the actor starred in. The first movie is the movie with the lowest#created_attimestamp.
Add
callbacks
Show archive.org snapshot
to your ActiveRecord models so the field is always up to date. Before you write a callback, list every change to roles and movies that can make the cache wrong, and write a test for each. For the purpose of this exercise, please don't use the :counter_cache option and do everything manually instead.
Whenever you add a new column you need to take care of existing records. Add a migration that sets the total_movies and first_movie_id column for existing records. Write this migration in multiple styles:
- With a minimal model class defined inside the migration, not your app's
Actor - SQL statements (
update "UPDATE actors SET ...")
Migrations and feature branches
- Make sure your backfill migration can be undone: run
rails db:rollback, thenrails db:migrateagain, and check that both work. - Now switch to
mainwithout rolling back first. Runrails db:migrate:statusand look atdb/schema.rbafter the next migration onmain. What went wrong? - Clean up: switch back, roll back, switch again.