Rules for Rails Migrations

2013-03-26

I see a lot of developers, even smart and experienced ones, get frustrated by Rails migrations—or cause frustration for others on their team. So here is a quick overview of how they work, plus a few rules for handling them without headaches. If you follow these rules, your migrations can be easy and smooth. Perhaps you’ll even stop wishing you could use Mongo.

Overview

A migration is a Ruby script with an up method containing the database changes you’d like applied, along with a down method to undo those changes and restore the database to its old structure. In newer versions of Rails, you can also write a single change method, and if you restrict yourself to the right methods, Rails will automatically understand how to undo your changes.

Migration scripts live in db/migrate, and they are named with a timestamp plus description, like this:

20121024000909_create_users.rb

or this:

20121102162841_add_address_to_profile.rb

You can create a new migration by saying rails g migration create_users, and Rails will automatically give it a timestamp and start the file. (Tip: If you’re using the command line, you can follow up that command with vi db/migrate/*!$* to open the migration in a text editor.)

By convention, migrations are named create_foo when creating a brand-new table, and add_foo_to_bar when adding the column foo to the table bar. But you can name them whatever you like. Note each migration needs a unique name, despite the timestamp, because each migration lives in a Ruby class whose name is the migration name camelized.

You can run the latest migrations by saying rake db:migrate, and you can undo the last-run migration with rake db:rollback. (You can run a limited number of migrations with rake db:migrate STEP=3). Rails automatically keeps track of which migrations you’ve applied in the schema_migrations table, which has just a single column listing the timestamps of all applied migrations.

Rules

Always implement the down method.

You might be tempted to leave out the down method, but it’s very useful to fill it in. Everyone makes mistakes, and the down method ensures you can recover. It also means that if your migration is not quite right the first time, you can db:rollback, make your fixes, and then db:migrate again. (But see below about pushed migrations.)

You may run into trouble if a migration fails halfway through. If this happens, the migration is not recorded in schema_migrations, but it might leave tables/columns in your database. Rather than dropping these, I prefer to simply comment out the lines that ran successfully, then restore them once the migration has succeeded. This works both when migrating and when rolling back.

A corollary to “implement the down method” is “test the down method.” I don’t mean write unit tests. I just mean when you think everything is correct, run rake db:rollback db:migrate and make sure it all works. It’s easy to have errors in your down methods if you never run them.

Don’t edit pushed migrations.

It’s fine to rollback, edit, and retry when you’re just working on new code that is private to your repo, but never edit a migration after sharing it with other developers, e.g. after doing a git push. Even safer would be never edit a migration after committing it. If you do this, you’re very likely to create out-of-sync databases for other developers and on production. This is because they may have already run the migration before your changes got pushed, and since they’ve already run it, they’ll never run it again to pick up your changes. Now your database looks different than everyone else’s.

I’ve seen this happen many times, and it’s probably the top cause of frustrations with Rails migrations. Recovering from it generally involves surgery, and unless the surgeon is patient and careful, he’s likely to make things even worse. To avoid the problem, if you’ve pushed a migration that you find is not quite right, always make your changes by adding a new migration, not by editing the old one.

Surgery that can help (if done right) is to delete rows from schema_migrations and manually add/remove/alter tables and columns to get back on track. Whatever the techniques, your goal should be to bring the databases into line with the accepted version history, so that running the migrations from scratch would produce the same database that comes out of your operating room. Otherwise you’re creating a time-bomb that will produce more out-of-sync databases somewhere down the line.

Stick to SQL.

Opinions differ on how to create a new database from scratch, for instance when a new developer joins or you decide to add that CI server. Some people like to create it from the schema.rb, like Athena springing full-grown from Zeus’s head. Others like to start with an empty database then run all the migrations from the beginning of the project (which could be years old). Other just make a dump from somewhere and import it where needed. Whatever your approach, there is value in at least striving for a non-broken migration history, so that ideally you can run all the migrations against an empty database. The closer you are to this ideal, the easier it will be to go back in time or handle branches.

This goal means your migration scripts should not rely on the rest of your source code. It’s common for migrations to use model classes, but don’t! Here is a place you should defy the DRY imperative. The reason is that when you run a 6-month-old migration, your model classes have today’s code, so it’s very easy to have missing methods, changed methods, renamed methods, even missing classes.

The best way to avoid problems is to keep your migrations entirely self-contained. For this I recommend writing data changes (i.e. DML changes) in pure SQL. You can do it like this:

ActiveRecord::Base.connection.execute(<<-EOQ)
    UPDATE  foo
    SET     bar = baz
    WHERE   ick = ack
EOQ

If you don’t like SQL, you’ll just have to deal. It’s good for you. Or change jobs and write Javascript for a living. :-P

Conclusion

Follow these rules to get frustration-free Rails migrations. To sum up, they are:

  • Write and test the down method.
  • Don’t edit pushed migrations.
  • Keep your migrations self-contained (ideally just SQL).

Defy them to your peril!

blog comments powered by Disqus Prev: Paperclip with Server-Side Files Next: Fun Postgres Puzzle