Getting Better at Messing Up Databases: Active Record Rollback Tips

Database management can be a real pain in the patooty. Putting aside the grammar and syntax that must be learned, it’s easy to realize that the tables or columns you’ve been working on aren’t what the problem at hand calls for.

Fortunately, Active Record has a tool that lets you reverse incorrect migrations (kind of). Introducing…


What does db:rollback actually do?

db:rollback “undoes” the last db:migrate recorded in the schema.rb file of your project.

In other words, db:rollback doesn’t internally keep track of whatever your last db:migrate was; instead, it references your schema file and attempts to undo the last db:migrate it can find there. This means that if you switch github branches on a project your db:rollback will undo the last db:migration on that branch.

There are some things db:rollback can’t normally undo, however.

db:rollback has trouble undoing some deletion migrations without special code. This is because db:rollback works by running the inverse process of the one specified in your last migration file.

For example, let’s create a table “Hunks”.

Now, let’s run rake db:migrate in our terminal.

What a nice Hunks table!

Wait! We've made a terrible mistake! We forgot to include the colors of the various hunks' hair! We'd better add that column to our table...

legolas.hair_color=”Hunky Elven Blonde”

That was a close call, but now we’re tracking the colors of those luscious locks. What happens if we decide later that we don’t care about the color of our hunks’ hair? Are all hunks not hunks regardless of their coloring? Let’s run another migration to remove that column.

On second thought, some people may want to know the hunk hair colors. Rather than run a new migrate, let’s just rollback that last migrate and delete the previous migration file so it isn’t run again…

rake db:rollback

=> rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

remove_column is only reversible if given a type.

When db:rollback attempts to undo the remove_column change, it does so by attempting to run add_column with the arguments you specified in your remove_column call. However, add_column and remove_column don’t require the same arguments. Creating columns requires the table name, column name, and datatype, whereas removing them only requires table name and column name. If only we could provide our remove_column method with the data type

Let’s go back to our original remove_column call. Our error message said:

remove_column is only reversible if given a type.

According to the official Active Record Documentation, type comes after table_name and column_name (just like when adding a column) like so:

remove_column(table_name, column_name, type, options)

If we run our remove_column migration with the following code…

We can rollback our remove_column with no errors thrown.

class RemoveHunksHairColor < ActiveRecord::Migration[4.2] (called from load at /Users/Troy/.rvm/gems/ruby-2.3.0/bin/rake:22)
== 14 RemoveHunksHairColor: reverting =========================================
— add_column(:hunks, :hair_color, :string)
-> 0.0052s
== 14 RemoveHunksHairColor: reverted (0.0075s) ================================

In the above code we can even see add_column(:hunks, :hair_color, :string) being run to “undo” our removal.

It’s also possible to define separate up and down states for each of your migrations. Although change is generally easier to work with, if you’re worried about how parts of your database will be written or unwritten you can use the up and down syntax.

For example, using the same example of our RemoveHunksHairColor migration, if we write our removal migration as follows:

We can define separate db:migrate and db:rollback actions. The up call runs remove_column with our two arguments for table and column.

class RemoveHunksHairColor < ActiveRecord::Migration[4.2] (called from load at /Users/Troy/.rvm/gems/ruby-2.3.0/bin/rake:22)
== 14 RemoveHunksHairColor: migrating =========================================
— remove_column(:hunks, :hair_color)
-> 0.0056s
== 14 RemoveHunksHairColor: migrated (0.0057s) ================================

Our down call runs add_column with our three arguments when db:rollback is used.

class RemoveHunksHairColor < ActiveRecord::Migration[4.2] (called from load at /Users/Troy/.rvm/gems/ruby-2.3.0/bin/rake:22)
== 14 RemoveHunksHairColor: reverting =========================================
— add_column(:hunks, :hair_color, :string)
-> 0.0052s
== 14 RemoveHunksHairColor: reverted (0.0075s) ================================

The up and down syntax can be used to customize any migrate/rollback relationship, and I imagine it has uses beyond making sure you can rollback your column mistakes.

The db:rollback call itself can also accept some optional arguments:

rake db:rollback STEP=3

Calling db:rollback with a STEP argument will allow you to rollback multiple steps, determined by the number you provide as an argument.

rake db:rollback:redorake db:rollback:redo STEP=3

The :redo argument rolls back and then migrates back up, which seems to be primarily used to check for up and down errors (in other words, do the arguments of your down call mess up the table in a meaningful way). The STEP argument here has the same function: you can rollback x number of steps and then migrate them back.

Resources [1] [2]

Developer, Songwriter, Bugbear Monk

Developer, Songwriter, Bugbear Monk