Using reversible in Ruby

A complex migration may involve processes that Active Record does not know how to reverse. You can use reversible to specify what to do when a migration is run and when it needs to be rolled back. For example:

class ExampleMigration < ActiveRecord::Migration

  def change

    create_table :distributors do |t|

      t.string :zipcode

    end

    do |dir|

      dir up do

        # add a CHECK constraint

        execute<<-SQL

          ALTER TABLE distributors

            ADD CONSTRAINT zipchk

              CHECK (char_length(zipcode) = 5) NO INHERIT;

        SQL

      end

      dir.down do

        execute<<-SQL

          ALTER TABLE distributors

            DROP CONSTRAINT zipchk

        SQL

      end

    end

    add_column :users, :home_page_url, :string

    rename_column :users, :email, :email_address

  end

end

Using reversible ensures that the instructions are executed in the correct order. If the previous migration example is rolled back, the down block will run after the home_page_url column is dropped and before the distributors table is dropped.

Sometimes a migration will do things that are simply irreversible; for example, it may destroy some data. In such cases, you can call ActiveRecord::IrreversibleMigration in your down block. If anyone tries to undo your migration, an error will be displayed stating that it can’t be done.

Using up/down methods

You can also use the old style of migrations using the up and down methods instead of change. The up method should describe the changes you want to make to your schema, and the down method of your migration should reverse the changes made by the up method. In other words, the database schema must remain the same after executing up and then down. For example, if you created a table in the up method, it should be deleted in the down method. It is reasonable to undo changes in the exact opposite order to the one in which they were made in the up method. Then the example from the reversible section would be equivalent to:

class ExampleMigration < ActiveRecord::Migration[5.0]

  def up

    create_table :distributors do |t|

      t.string :zipcode

    end

    #add a CHECK constraint

    execute<<-SQL

      ALTER TABLE distributors

        ADD CONSTRAINT zipchk

        CHECK (char_length(zipcode) = 5);

    SQL

    add_column :users, :home_page_url, :string

    rename_column :users, :email, :email_address

  end

  def down

    rename_column :users, :email_address, :email

    remove_column :users, :home_page_url

    execute<<-SQL

      ALTER TABLE distributors

        DROP CONSTRAINT zipchk

    SQL

    drop_table :distributors

  end

end

If your migration is not reversible you should call ActiveRecord::IrreversibleMigration from your down method. If anyone tries to undo your migration, an error will be displayed stating that it can’t be done.

(reverting-previous-migrations) Reverting to previous migrations

You can use the Active Record feature to roll back migrations using the revert method:

require_relative ‘20121212123456_example_migration’

class FixupExampleMigration < ActiveRecord::Migration[5.0]

  def change

    revert ExampleMigration

    create_table(:apples) do |t|

      t.string :variety

    end

  end

end

The revert method can also accept a block. This can be useful for rolling back a selected part of previous migrations. As an example, let’s imagine that ExampleMigration is committed, and later we decide that it would be better to use Active Record validations, instead of the CHECK constraint, to check the zipcode.

A similar migration could also be written without using revert, but that would involve a few more steps: reordering create table and reversible, replacing create_table with drop_table, and eventually changing up to down and vice versa. Revert has already taken care of all this.

NOTE: If you want to add CHECK constraints like in the examples above, you must use structure.sql as the export method. See Export Schema.

Running migrations

Rails provides a number of bin/rails tasks to run specific sets of migrations.

The very first bin/rails task migration we’ll be using is rails db:migrate. In its main form, it just runs the change or up method on all migrations that haven’t been run yet. If there are no such migrations, it exits. It will run these migrations in order based on the date of the migration.

Note that running the db:migrate task also invokes the db:schema:dump task, which updates your db/schema.rb file to match your database structure.

If you specify a target version, Active Record will run the required migrations (up, down, or change methods) until it reaches the required version. Version is a numeric prefix for the migration file. For example, to migrate to version 20080906120000, run:

$ bin/rails db:migrate VERSION=20080906120000

If version 20080906120000 is greater than the current version (i.e. forward migration) this will run the change (or up) method on all migrations up to and including 20080906120000, but will not run any later migrations. If the migration is backward, this will run the down method on all migrations up to, but not including, 20080906120000.

Rollback

A common task is to rollback the last migration. For example, you made a mistake and want to fix it. You can track down a version of a previous migration and migrate to it, but you can do it easier by running:

$ bin/rails db:rollback

This will revert the situation to the last migration, either by reversing the change method or by running the down method. If you need to undo multiple migrations, you can specify the STEP option:

$ bin/rails db:rollback STEP=3

there will be a rollback to the last 3 migrations.

The db:migrate:redo task is a shortcut to perform a rollback and then run the migration again. Just as with the db:rollback task, you can specify the STEP option if you need to work with more than one version, for example:

$ bin/rails db:migrate:redo STEP=3

None of these bin/rails tasks can do anything that can’t be done with db:migrate. They’re just more convenient because you don’t have to explicitly specify which migration version to migrate to.

Database installation

The rails db:setup task will create the database, load the schema, and initialize it with the seed data.

Reset database

The rails db:reset task will remove the database and reinstall it. This is functionally equivalent to rails db:drop db:setup.

NOTE. This is not the same as running all migrations. It only uses the current contents of the db/schema.rb or db/structure.sql file. If the migration cannot be rolled back, rails db:reset may not help you. For more information about exporting a schema, see Exporting a Schema.

Running specific migrations

If you need to run a specific migration (up or down), the db:migrate:up and db:migrate:down tasks will do that. Just define the appropriate option and the appropriate migration will have its change, up or down method called, like so:

$ bin/rails db:migrate:up VERSION=20080906120000

will run the up method on migration 20080906120000. This task will first check if the migration has already been run, and will do nothing if Active Record thinks it has already run.

Running migrations in different environments

By default, running bin/rails db:migrate will run in the development environment. To run migrations in a different environment, it can be specified using the RAILS_ENV environment variable when running the command. For example, to run migrations in the test environment, you would run:

$ bin/rails db:migrate RAILS_ENV=test

Changing the output of running migrations

By default, migrations only tell us what they do and how long it took. A migration that creates a table and adds an index will output something like this:

== CreateProducts: migrating =========================================== ====

— create_table(:products)

   -> 0.0028s

== CreateProducts: migrated (0.0028s) =======================================

Some methods in migrations allow you to control all this:

Method Purpose

suppress_messages Takes a block as an argument and suppresses any output generated by this block.

say Takes a message as an argument and prints it as is. A second boolean argument can be passed to indicate whether indentation is needed or not.

say_with_time Prints the text along with the duration of the block. If the block returns a number, it is assumed to be the number of affected rows.

Leave a Reply

Your email address will not be published.