A migration is simply a Ruby source file in your application’s db/migrate directory. Each migration file’s name starts with a number of digits (typically fourteen) and an underscore. Those digits are the key to migrations, because they define the sequence in which the migrations are applied—they are the individual migration’s version number.
The version number is the Coordinated Universal Time (UTC) timestamp at the time the migration was created. These numbers contain the four-digit year, followed by two digits each for the month, day, hour, minute, and second, all based on the mean solar time at the Royal Observatory in Greenwich, London. Because migrations tend to be created relatively infrequently and the accuracy is recorded down to the second, the chances of any two people getting the same timestamp is vanishingly small. And the benefit of having timestamps that can be deterministically ordered far outweighs the miniscule risk of this occurring.
Here’s what the db/migrate directory of our Depot application looks like:
| depot> ls db/migrate |
| 20170425000001_create_products.rb |
| 20170425000002_create_carts.rb |
| 20170425000003_create_line_items.rb |
| 20170425000004_add_quantity_to_line_items.rb |
| 20170425000005_combine_items_in_cart.rb |
| 20170425000006_create_orders.rb |
| 20170425000007_add_order_id_to_line_item.rb |
| 20170425000008_create_users.rb |
Although you could create these migration files by hand, it’s easier (and less error prone) to use a generator. As we saw when we created the Depot application, there are actually two generators that create migration files:
The model generator creates a migration to in turn create the table associated with the model (unless you specify the --skip-migration option). As the example that follows shows, creating a model called discount also creates a migration called yyyyMMddhhmmss_create_discounts.rb:
| depot> bin/rails generate model discount |
| invoke active_record |
» | create db/migrate/20121113133549_create_discounts.rb |
| create app/models/discount.rb |
| invoke test_unit |
| create test/models/discount_test.rb |
| create test/fixtures/discounts.yml |
You can also generate a migration on its own.
| depot> bin/rails generate migration add_price_column |
| invoke active_record |
» | create db/migrate/20121113133814_add_price_column.rb |
Later, starting in Anatomy of a Migration, we’ll see what goes in the migration files. But for now, let’s jump ahead a little in the workflow and see how to run migrations.
Migrations are run using the db:migrate Rake task:
| depot> bin/rails db:migrate |
To see what happens next, let’s dive down into the internals of Rails.
The migration code maintains a table called schema_migrations inside every Rails database. This table has just one column, called version, and it will have one row per successfully applied migration.
When you run bin/rails db:migrate, the task first looks for the schema_migrations table. If it doesn’t yet exist, it will be created.
The migration code then looks at the migration files in db/migrate and skips from consideration any that have a version number (the leading digits in the filename) that is already in the database. It then proceeds to apply the remainder of the migrations, creating a row in the schema_migrations table for each.
If we were to run migrations again at this point, nothing much would happen. Each of the version numbers of the migration files would match with a row in the database, so there would be no migrations to apply.
However, if we subsequently create a new migration file, it will have a version number not in the database. This is true even if the version number was before one or more of the already applied migrations. This can happen when multiple users are using a version control system to store the migration files. If we then run migrations, this new migration file—and only this migration file—will be executed. This may mean that migrations are run out of order, so you might want to take care and ensure that these migrations are independent. Or you might want to revert your database to a previous state and then apply the migrations in order.
You can force the database to a specific version by supplying the VERSION= parameter to the rake db:migrate command:
| depot> bin/rails db:migrate VERSION=20170425000009 |
If the version you give is greater than any of the migrations that have yet to be applied, these migrations will be applied.
If, however, the version number on the command line is less than one or more versions listed in the schema_migrations table, something different happens. In these circumstances, Rails looks for the migration file whose number matches the database version and undoes it. It repeats this process until there are no more versions listed in the schema_migrations table that exceed the number you specified on the command line. That is, the migrations are unapplied in reverse order to take the schema back to the version that you specify.
You can also redo one or more migrations:
| depot> bin/rails db:migrate:redo STEP=3 |
By default, redo will roll back one migration and rerun it. To roll back multiple migrations, pass the STEP= parameter.