Blog

Reversible migrations with Active Record

By Glen Crawford, 17 Apr 2014

I figured I would take some time to write about a nifty feature that I'm not sure many developers know about, or have had an opportunity to use yet: the reversible method in Active Record migrations.

Introduced in Rails 4.0, reversible makes it possible to tell a migration using change (instead of up and down) how to reverse migrations that Active Record doesn't know how to reverse by default, so that you can specify code to be executed whether migrating forward or rolling back, even inside a migration implemented within a change method.

But first, let's make sure we understand change in migrations.

The change method

The change method is the new (as of Rails 3.1) and preferred way of writing migrations, replacing the up and down methods (although you can still use them if you want to). Using change, Active Record can automatically figure out how to reverse your migration, negating the need for you to implement both the up and down methods. For example, if you have a simple migration to add a new field, like:

def change
  change_table :products do |t|
    t.string :color
  end
end

Active Record knows that to reverse the migration, all it has to do is the equivalent of:

def down
  change_table :products do |t|
    t.remove :color
  end
end

Or even:

remove_column :products, :color

If you have multiple instructions in the migration, Active Record will also run the inverse instructions in reverse (i.e. with the last instructions being reversed first), just as they would have been written in a down method.

However, Active Record isn't magic. It can only automatically reverse migrations with a limited set of instructions. For example, add_column, add_index, create_table, and so on can all be easily reversed, as all Active Record has to do is drop the column, index or table. It is pretty easy for Active Record to figure out the inverse of these instructions. To see how this happens, you can peruse ActiveRecord::Migration::CommandRecorder, which also documents the limitations of Active Record's automatic reversing of migrations (it isn't as complicated as it sounds).

We can see this in action. If we have a migration with the following:

def change
  add_column :products, :size, :string
end

Then we can see Rails reversing it on rake db:rollback:

glen@~/rails_app > rake db:migrate
== 20140411040853 AddSizeToProducts: migrating ================================
-- add_column(:products, :size, :string)
   -> 0.0005s
== 20140411040853 AddSizeToProducts: migrated (0.0006s) =======================

glen@~/rails_app > rake db:rollback
== 20140411040853 AddSizeToProducts: reverting ================================
-- remove_column(:products, :size, :string)
   -> 0.0074s
== 20140411040853 AddSizeToProducts: reverted (0.0180s) =======================

However, what happens if you want to do something that Active Record can't automatically figure out how to reverse? For example, you might want to remove a column, say, a not-null decimal column (with precision and scale) called gst, as in the following:

def change
  change_table :products do |t|
    t.remove :gst
  end
end

The inverse of that is to create the column again, adding a column called gst to the products table. But there's more than that. The gst column had other properties: the decimal type, the not-null constraint, and the precision and scale. Thus, Active Record doesn't know everything that it needs to know to automatically add the gst column again and reverse the migration. So you will be greeted with an ActiveRecord::IrreversibleMigration exception when you run rake db:rollback. This would have been the same as if you had written:

remove_column :products, :gst

Another example would be changing the precision and scale of a decimal field. Consider the following migration to change a column that is currently a not-null decimal with a precision and scale of 7 and 2, respectively:

def change
  change_table :products do |t|
    t.change :price, :decimal, :precision => 10, :scale => 5
  end
end

Active Record can't automatically reverse this migration either, as it doesn't know what the previous precision and scale values were. Once again, rake db:rollback will fail with an ActiveRecord::IrreversibleMigration exception.

To handle this case, you would previously have been forced to split your migration into both up and down methods, and implement both. But now, with reversible, we no longer need to do that.

Using the reversible method

The reversible method lets you specify a block of code to be run in each direction, when migrating forward, or rolling back. The reversible method yields an instance of ActiveRecord::Migration::ReversibleBlockHelper. This class has only two very basic methods: up and down. Check out the source of this class:

class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
  def up
    yield unless reverting
  end

  def down
    yield if reverting
  end
end

This means that you can use reversible to define code to be executed when migrating or reverting, like so:

reversible do |dir|
  dir.up do
    # Run on rake db:migrate.
  end

  dir.down do
    # Run on rake db:rollback.
  end
end

So we can now write a migration with change that Active Record knows how to reverse, meaning we don't need to implement both the up and down methods. This can be done like so:

def change
  reversible do |dir|
    change_table :products do |t|
      dir.up   { t.change :price, :decimal, :precision => 10, :scale => 5 }
      dir.down { t.change :price, :decimal, :precision => 7, :scale => 2 }
    end
  end
end

And in the case of our previously-irreversible migration to remove the gst field from the products table, we can now implement it like this, so that it is fully reversible:

def change
  reversible do |dir|
    change_table :products do |t|
      dir.up   { t.remove :gst }
      dir.down { t.decimal :gst, :precision => 7, :scale => 2 }
    end
  end
end

Now that we're back at using up and down again, you might be wondering why you should use up/down blocks when you could just use up/down methods, like you used to. For starters, it can save you a bit of repetition; if you had two separate methods you would need to write out the change_table :products do...end lines twice. But more importantly, you will often find that your migrations are more complex than the ones in these examples, with more instructions. If all of the instructions in a migration were reversible except for one, it would be a pain to have to split it into two methods and write out the down method by hand. With reversible, you only need to define the inverse of the instructions that need it, while still enjoying the benefits of Active Record automatically taking care of the rest of them.

One more thing: reversible remove_column

As of Rails 4.0, remove_column can now be reversed, with just a little bit more effort. You can pass in the column's type and options (default, limit, etc.) into remove_column. It may seem a bit redundant to do so, but when used inside a migration within a change method, those options will be converted into an add_column call, and the instruction becomes reversible.

Conclusion

And that's all there is to it! You now know the ins and outs (or ups and downs!) of how Active Record can, in most cases automatically reverse migrations without you needing to implement a down method. And by combining the change method with the reversible method, can now pull it off for the cases where Active Record can't figure it out on its own, without you having to split your change method into two up and down methods and implement both. This makes your migrations faster to write and less repetitive.

blog comments powered by Disqus