Healthy Migrations

| Comments

Continuing with the fixtures/test theme, I want to focus on the place where fixtures actually live - the database. Migrations are the blueprint, however, they often break and we don’t notice. You should alway be able to do this:

1
2
$ rake db:migrate VERSION=0
$ rake db:migrate

I used to say “what does it matter? We’re never going back to migration 3, we’re on 156 now!” This kind of thinking showed how I didn’t understand the usefulness of migrations:

  • Setting up a new development system should not require a recent database snapshot.
  • Automated tests and build notifications are simpler when the migrations are clean.
  • cap rollback will save your life some day

Migrations are not just a historical record of your database design. They instead give you a way to build your database up from scratch, doing more than just creating a schema. You can seed data, create indexes, and make transformations.

When you first start a rails project, and everything is golden, migrations are easy. Eventually, you run into problems. It happens a lot, because we typically don’t test the entire migration sequence. For example:

A model changes somewhere, and breaks a dependent migration

Using models in migrations is a common way to seed the database, or manipulate things:

1
2
3
4
5
6
7
8
9
class CreateNewsSection < ActiveRecord::Migration
  def self.up
    Section.create(:name => 'news', :title=>"News")
  end

  def self.down
    Section.find_by_name('news').destroy
  end
end

If you delete or refactor the Section model later, this migration will likely break. The solution for this one is to define the model in the migration:

1
2
3
4
5
6
class CreateNewsSection < ActiveRecord::Migration
  class Section < ActiveRecord::Base; end
  def self.up
    Section.create(:name => 'news', :title=>"News")
  end
  ... etc.

Someone on the team checks in a migration that has a bug

If the problem is trivial, they might be tempted to skip reporting it and just fix it in the database to keep things moving. Or, they may not even notice the problem, depending on when they updated. These issues can lead to inconsistencies, and tests that pass for one developer, but not another!

Developers only migrate up

Migrating down should work too, what if you need to roll back to fix something in production? Always write a sensible down method and test it. It does not have to perfectly reverse the database, it just needs to return it to a state that will enable the previous migration to run it’s down method. I’ve seen horrific migrations checked in like this:

1
2
3
  def self.down
    # no need to do this
  end

The team works from a production db snapshot based on a deployed site

This is bad, because it means the team is probably not using TDD, and are instead relying on browser interaction to develop the app. At minimum, they are blind to migration issues. Relying on an external database for development is an unwise dependency. It also complicates setup for testing.

Keep Migrations Working!

Each time you add a migration, or refactor a number models, you should check that all the migrations are working. There are a number of solutions for doing this - the most obvious is to drop the dev db and migrate up from scratch, see if it works.

Err the blog posted a task a while back. There’s also this often referenced snippet that works. And today, I noticed this post on Ryan’s Scraps – it looks like Rails itself now has a task to do this.

However, my favorite solution at the moment is sitting in a patch #8389 (not committed at this date), which offers this bit of sugar:

1
2
# in config/environment.rb:
config.active_record.schema_format = :migrations

This setting would force rails to build the database schema from migrations, not from sql or db/schema.rb.

So before you check in migrations, make sure you can run them up from scratch. And then, don’t forget to make sure your fixtures are still valid, too!

Comments