An introduction to

Database Migrations in Ruby on Rails

by

Øyvind Guttvik Årnes

Overview

  • A feature that allows you to evolve your database schema over time
  • Use Ruby rather than SQL to change tables
  • Each migration is a "new" version of the database, and modifies the schema to add or remove tables, columns, or entries
  • In Rails each migration is stored in the /db/migrate folder in the format YYYYMMDDHHMMSS_name.rb
  • Database independent
  • Part of Rails Active Record (ORM)

Example migration

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
      t.timestamps null: false #modifier
    end
  end
end

Using up/down

class CreateProducts < ActiveRecord::Migration
  def up
    create_table :products do |t|
      t.string :name
      t.text :description
      t.timestamps null: false #modifier
    end
  end
  def down
    drop_table :products
  end
end

Simple schema evolving

1_create_products.rb          ****************
                              *   Products   *
                              ****************
                              * id           *
                              * name         *
                              * description  *
                              ****************

2_create_customers.rb         ****************  ****************  
                              *   Products   *  *   Customers  *
                              ****************  ****************
                              * id           *  * id           *
                              * name         *  * name         *
                              * description  *  * email        *
                              ****************  ****************

3_add_phone_to_customer.rb    ****************  ****************  
                              *   Products   *  *   Customers  *
                              ****************  ****************
                              * id           *  * id           *
                              * name         *  * name         *
                              * description  *  * email        *
                              ****************  * phone        *
                                                ****************

Creating migrations

Using a generator

$ bin/rails generate migration AddPhoneToCustomers

will create

class AddPhoneToCustomers < ActiveRecord::Migration
  def change
  end
end

More params

$ bin/rails generate migration AddPhoneToCustomers phone:string

will create

class AddPhoneToCustomers < ActiveRecord::Migration
  def change
    add_column :customers, :phone, :string
  end
end

Model generator

$ bin/rails generate model Product name:string description:text

will create

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
      t.timestamps null: false
    end
  end
end

In addition to the model itself

Migration methods

Create table

create_table :products do |t|
  t.string :name
end

will implicitly create a primary key called id. Name can be changed using :primary_key option. If you dont want a primary key, pass option id: false

Create join table

create_join_table :products, :categories

Creates a HABTM(has and belongs to many) join table.

***********************
* categories_products *
***********************
* category_id         *   
* product_id          *
***********************

Changing tables

change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end

Change columns

change_column :products, :part_number, :text

note: change_column is irreversible

Column Modifiers

Can be applied when creating or changing columns

  • limit
  • precision
  • scale
  • polymorphic
  • null
  • default
  • index

Foreign keys

add_foreign_key :articles, :authors

Adds a foreign key to the author_id column of the articles table.

Available options: :column :primary_key :name

Removing

# let Active Record figure out the column name
remove_foreign_key :articles, :authors

# remove foreign key for a specific column
remove_foreign_key :articles, column: :author_id

# remove foreign key by name
remove_foreign_key :articles, name: :special_fk_name

Using SQL

Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")

The change method

Primary way of writing migrations. Active Record knows how to reverse.

Supported definitions:

add_column          add_foreign_key       add_index

add_reference       add_timestamps        change_column_default (must supply a :from and :to option)

change_column_null  create_join_table     create_table

disable_extension   drop_join_table       drop_table (must supply a block)

enable_extension    remove_index          remove_column (must supply a type)

remove_reference    remove_timestamps     remove_foreign_key (must supply a second table)

rename_column       rename_index          rename_table

Using reversible

when Active Record doesn't know how to reverse

class ExampleMigration < ActiveRecord::Migration
  def change
    create_table :distributors do |t|
      t.string :zipcode
    end
    reversible 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 up/down

"Old style"

class ExampleMigration < ActiveRecord::Migration
  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

Running migrations

Rake tasks

$ bin/rake db:migrate

Will run all migrations that have not yet been run.

$ bin/rake db:migrate VERSION=20150906120000

Will run up or down to specific version. Will not run down on this version if moving down.

$ bin/rake db:rollback

Revert last migration

$ bin/rake db:rollback STEP=3

Revert last 3 migrations

$ bin/rake db:setup

Create the database, load the schema.

$ bin/rake db:reset

Drop the database and set it up again. Equivalent to rake db:drop db:setup

Changing existing migrations

Must rollback, edit, then rake db:migrate.

Should not edit migrations that are committed to source control. Create a new migration that performs the changes you require.

Schema Dumping

Migrations are not the authoritative source for your database schema.

db/schema.rb or an SQL file has that role. They should not be edited.

I deploying a new instance of an application. No need to run all migrations, just load the schema. db:setup (db:schema:load)

The schema can be dumped as SQL or ruby depending on setting config.active_record.schema_format in config/application.rb

# db/schema.rb
ActiveRecord::Schema.define(version: 20150906171750) do
  create_table "authors", force: true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "products", force: true do |t|
    t.string   "name"
    t.text "description"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string "part_number"
  end
end

The ruby version will be database-independent and can be loaded in any database that Active Record support. But this cannot express database specific items (triggers, stored procedures or check constraints). If using features like this, use the SQL version.

When using SQL. The schema dump will be database-specific and stored in db/structure.sql

My experience

  • Like that you evolve the database "inside" the project
  • Easier to keep database changes in source control.
  • Database independence - start developing in SQLite, move to other.
  • Deploying updates via migrations.
  • Could be hard to use migrations when developing a large system and Rails is just a small part.