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:
ruby
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:
ruby
def down
change_table :products do |t|
t.remove :color
end
end
Or even:
ruby
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:
ruby
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
== 201Matenia Rossides4110Matenia Rossides853 AddSizeToProducts: migrating ================================
-- add_column(:products, :size, :string)
-> 0.0005s
== 201Matenia Rossides4110Matenia Rossides853 AddSizeToProducts: migrated (0.0006s) =======================
glen@~/rails_app > rake db:rollback
== 201Matenia Rossides4110Matenia Rossides853 AddSizeToProducts: reverting ================================
-- remove_column(:products, :size, :string)
-> 0.0074s
== 201Matenia Rossides4110Matenia Rossides853 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:
ruby
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:
ruby
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:
ruby
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:
ruby
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:
ruby
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:
ruby
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:
ruby
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.