Why Reversible Migrations Are Important and How to Write Them?

   Back to list

Introduction

I think that everyone who uses Rails knows what a migration is and how to use it. If you are not familiar with Rails migrations, I’ll try to introduce you into the subject.

Migrations are not only used in Rails, but you can find them in Django, Laravel or even in .NET MVC – they look pretty similar, and they have the same responsibility and goal.

Migrations are files, which are responsible for making any changes in the database. They can create or drop a table, add or remove a column, modify it, add an index or rename it or do plenty of different thing. They can also contain a code which makes some changes in the existing data, for example, update it or remove records which are invalid.

They help you to evolve your database schema over time without a problem. They convert a Ruby code into a SQL code, so you don’t even write a single line of SQL code.

Database Schema

As I stated before, migrations help to manage with a database schema. But what is the database schema? The schema is a file, which keeps information about your database, tables structure, information about indexes, foreign keys and added extensions.

An actual database schema could be find the db/schema.rb file, it looks something like:

ActiveRecord::Schema.define(version: 2018_07_31_134814) do

 # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "authors", force: :cascade do |t|
    t.string "first_name"
    t.string "last_name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end

Migration file

In Rails, when you want to create a table (probably a model), you can do it like:

$ rails g model author first_name last_name

You probably noticed, that not only a model is generated when you request to create it but Rails also adds a few more files:

Running via Spring preloader in process 38927
      invoke  active_record
      create    db/migrate/20180731134814_create_authors.rb
      create    app/models/author.rb
      invoke    rspec
      create      spec/models/author_spec.rb

If you use RSpec, it creates for you a model’s spec file. ActiveRecord also creates a migration, which will create a new table in the existing database, normally it looks something like that:

class CreateAuthors < ActiveRecord::Migration[5.2]
  def change
    create_table :authors do |t|
      t.string :first_name
      t.string :last_name

      t.timestamps
    end
  end
end

When you’ll run the migration, Rails returns an output which tells how much time took to run it:

$ rake db:migrate
== 20180731134814 CreateAuthors: migrating ====================================
-- create_table(:authors)
   -> 0.0658s
== 20180731134814 CreateAuthors: migrated (0.0659s) ===========================

Reversible migrations

If you used an older version of Rails previously, for example, version 3.0, you probably remember, that migration files looked a bit different, by default, there were two methods instead of one – change method. There was an up method and a down method. The up method was executed when you run a migration file, but the down method was executed when you reversed your changes by running the rake db:rollback command.

Yes, migrations are reversible! What does it mean? When you run a migration, it runs by a default the up method which is hidden under the change method, so when you want to create a table, or add a column, it will do this change, but when you revert this change, it’ll drop this table or remove the column – in this case, the down method is called.
But let’s be real, you don’t want only to add a new table or a new column, sometimes you also want to remove a column or a table. In this case, the up method will remove it, but the down method will add it again to the table – but without the old data, it’s imperative!

We should keep in mind, that when you remove a table and later revert this change, it will be created again, but it will be empty!
Rails are not able to restore data, because they convert a Ruby code into SQL queries and run them, like:

DROP table authors;

So if you want to convert our migration file into the old migration styled file, it should look like:

class CreateAuthors < ActiveRecord::Migration[5.2]
  def up
    create_table :authors do |t|
      t.string :first_name
      t.string :last_name

      t.timestamps
    end
  end

  def down
    drop_table :authors
  end
end

Ok, now let’s try to revert our changes, let’s run the rollback method:

$ rake db:rollback
== 20180731134814 CreateAuthors: reverting ====================================
-- drop_table(:authors)
   -> 0.0258s
== 20180731134814 CreateAuthors: reverted (0.0381s) ===========================

As you can see, our changes have been reverted successfully.

Reversible migrations are a pretty cool feature! When we run a migration by mistake or it’s incorrect, we can revert it and start from scratch – but we should remember that our data won’t be restored – it’s vital!

More advanced examples

Previously, we focused on straightforward examples, now let’s move to something more complicated.

Let’s imagine, that we need to make a complex and complicated migration and we aren’t able to write this code in ActiveRecord, we need to execute pure SQL query, so we need to run the execute method like:

def change
  execute(‘AN ADVANCED SQL QUERY’)
end

What will happen when you try to run a migration and then revert it later? Let’s try it:

$ rails g migration complex_sql_query
class ComplexSqlQuery < ActiveRecord::Migration[5.2]
  def change
    execute("INSERT INTO authors(first_name, last_name, created_at, updated_at) VALUES ('Foo', 'Bar', '2018-07-31', '2018-07-31');")
  end
end
$ rake db:migrate

== 20180731152957 ComplexSqlQuery: migrating ==================================
-- execute("INSERT INTO authors(first_name, last_name, created_at, updated_at) VALUES ('Foo', 'Bar', '2018-07-31', '2018-07-31');")
   -> 0.0015s
== 20180731152957 ComplexSqlQuery: migrated (0.0016s) =========================

$ rake db:rollback

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

This migration uses execute, which is not automatically reversible.
To make the migration reversible, you can either:
1. Define #up and #down methods in place of the #change method.
2. Use the #reversible method to define reversible behavior.

Caused by:
ActiveRecord::IrreversibleMigration

Yes, as you can see, there is no possibility to revert this migration, automatically Rails thrown ActiveRecord::IrreversibleMigration exception.

Now let’s imagine, that you want to revert your migration on production, because it turned out, that somehow, it’s invalid (but anyway, we shouldn’t do that, this situation shouldn’t ever happen), or you just pulled the newest changes from git to your branch and you just want to undo them but you’ve already run the migration.

Now, you see that it’s vital to write reversible migrations, because they can save a lot of time and nerves.
Probably there is a lot of different cases where reversible migrations could help you, it’s important to think about them. If a migration can be processed, we also should be able to revert it!

Irreversible migrations

If we want to have an irreversible migration, it should be well documented and included in the down method by throwing an ActiveRecord::IrreversibleMigration exception.

Now, we have a few possibilities and options how we can rewrite this migration.
In the first example, we’ll rewrite the update method to up and down and raise an exception, like:

class ComplexSqlQuery < ActiveRecord::Migration[5.2]
  def up
    execute("COMPLEX SQL QUERY")
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end
end

Here, we throw an exception and notify a user, that this migration shouldn’t be reverted. That shows a user that he shouldn’t revert it because we did something which can’t be restored.

Reversible migration – not the good way

Another thing which we can do is to modify the down method and allow to revert this migration and prompt a message:

class ComplexSqlQuery < ActiveRecord::Migration[5.2]
  def up
    execute("COMPLEX SQL QUERY")
  end

  def down
    puts 'This migration makes some breaking changes, should not be reverted.'
  end
end

In this particular example, we allow a user to revert the migration, but we should think if we want to do it. Let’s imagine, that this migration will be reverted and the user will rerun it. Maybe it’ll break some data in our database!

Reversible block

Another useful method is the reversible block. In this block, we can define which part of our migration should be reverted and how should it be handled. Let’s check more complex example:

class ReversibleMigration < ActiveRecord::Migration[5.2]
  def change
    create_table :authors do |t|
      t.string :first_name
      t.string :last_name
    end
 
    reversible do |migration|
      migration.up do
        execute <<-SQL
          SOME SQL QUERY
        SQL
      end
      migration.down do
        execute <<-SQL
          SOME SQL QUERY
        SQL
      end
    end
 
    add_column :authors, :age, :integer
    rename_column :authors, :last_name, :surname
  end
end

As you can see, we don’t even need to write the up and down methods. We only need to include them only in the reversible block, so in this case, only something more complex will be run separately in these blocks. When we roll back our changes, Rails knows how to run create_table, add_column and rename_column methods and restore everything, so we don’t need to include them in separate blocks!

I think that this migration is more clear and easier to read, we define which changes are handled by us manually, and which by Rails.

The revert method

There is one more cool thing in migrations; if you want to revert one migration by including it in another one, you can do it too! Let’s imagine, that you want to revert one migration which added one field into a table and also add another field to the same table or you want to do something completely different.
There are two ways how can we do it:

  • write two separate migrations
  • use revert method in a migration file with a migration name as a parameter

Let’s assume that we want to revert a migration which is called MyMigrationNumberOne and create another one, in this case, we can create something like this:

require_relative '20180101101112_my_migration_number_one'
 
class NewMigration < ActiveRecord::Migration[5.2]
  def change
    revert MyMigrationNumberOne
 
    create_table(:new_table) do |t|
      t.string :some_field
    end
  end
end

Now, when we run this migration, the MyMigrationNumberOne migration will be reverted, and at the same time, we’ll create a new table – pretty impressive, isn’t it?

Send this to a friend