Skip to content

Ruby on Rails - Migrations

Rails Migrations are a way to manage your database schema changes over time. Instead of writing SQL DDL (Data Definition Language) directly, you define schema changes in Ruby using a Domain-Specific Language (DSL). This allows your schema changes to be database-agnostic (mostly) and version-controlled along with your application code.

Benefits of using migrations:

  • Collaboration: If one developer makes a schema change, others can update their local databases by running rails db:migrate.
  • Deployment: Simplifies updating the production database schema during deployments.
  • Version Control: Schema changes are part of your codebase and tracked in version control (e.g., Git).
  • Database Independence: Migrations abstract away many database-specific SQL syntaxes.
  • create_table(name, options): Creates a new table.
  • drop_table(name): Deletes a table.
  • rename_table(old_name, new_name): Renames a table.
  • add_column(table_name, column_name, type, options): Adds a new column to a table.
  • rename_column(table_name, old_column_name, new_column_name): Renames an existing column.
  • change_column(table_name, column_name, type, options): Changes the type or options of a column.
  • remove_column(table_name, column_name, options): Removes a column.
  • add_index(table_name, column_name_or_columns, options): Adds an index to one or more columns.
  • remove_index(table_name, options_or_column_name): Removes an index.
  • add_reference(table_name, ref_name, options) or add_belongs_to: Adds a foreign key reference.
  • t.timestamps: A helper within create_table block that adds created_at and updated_at columns.

Supported Data Types in Migrations:

  • :string (for short text, e.g., titles)
  • :text (for longer text, e.g., descriptions)
  • :integer (for whole numbers)
  • :bigint (for larger whole numbers)
  • :float (for floating-point numbers)
  • :decimal (for precise decimal numbers, often used for currency)
  • :datetime (for date and time)
  • :timestamp (similar to datetime)
  • :date (for date only)
  • :time (for time only)
  • :binary (for binary data)
  • :boolean (for true/false values)

Common Column Options: :limit, :precision, :scale (for decimals), :default, :null (boolean), :index (boolean or options hash).

You can generate migration files using the rails generate migration command. Rails uses a timestamp in the filename to ensure migrations are run in order.

Example: Generating migrations for our books and subjects tables (if not already generated by rails g model):

$ bin/rails generate migration CreateBooks title:string price:decimal:10,2 subject_id:bigint:index description:text published_at:date
$ bin/rails generate migration CreateSubjects name:string:uniq

This will create files like db/migrate/YYYYMMDDHHMMSS_create_books.rb and db/migrate/YYYYMMDDHHMMSS_create_subjects.rb.

If you use rails generate model Book title:string ..., the migration is created automatically. The example above shows how to create migrations standalone. Notice :index creates an index on subject_id, and :uniq creates a unique index on name for subjects.

Let’s look at the content of a generated (or manually created) migration file. For CreateBooks:

db/migrate/YYYYMMDDHHMMSS_create_books.rb
class CreateBooks < ActiveRecord::Migration[7.0] # Use your current Rails version
def change
create_table :books do |t|
t.string :title, null: false
t.decimal :price, precision: 10, scale: 2
# t.references :subject, null: false, foreign_key: true # Preferred way for foreign keys
t.bigint :subject_id # If not using t.references
t.text :description
t.date :published_at
t.timestamps # Adds created_at and updated_at columns
end
# Add index separately if not using t.references with index: true or if subject_id was manual
add_index :books, :subject_id unless index_exists?(:books, :subject_id)
add_index :books, :title # Example: adding an index on title
end
end

For CreateSubjects:

db/migrate/YYYYMMDDHHMMSS_create_subjects.rb
class CreateSubjects < ActiveRecord::Migration[7.0]
def change
create_table :subjects do |t|
t.string :name, null: false
t.timestamps
end
add_index :subjects, :name, unique: true unless index_exists?(:subjects, :name, unique: true)
end
end
  • ActiveRecord::Migration[7.0] specifies the Rails version this migration was written for.
  • The change method is used for migrations that Active Record knows how to reverse automatically (e.g., create_table can be reversed by drop_table).
  • For non-reversible changes, you’d define separate up (to apply changes) and down (to revert them) methods.
  • t.timestamps is a helper that adds created_at and updated_at columns, which Active Record manages automatically.
  • null: false makes a column required at the database level.
  • t.references :subject, foreign_key: true is a convenient way to add a subject_id column, an index on it, and a foreign key constraint.

Important: Avoid putting data seeding (e.g., Subject.create(...)) inside migration files. Migrations are for schema changes. Data seeding should go in db/seeds.rb or custom Rake tasks.

To apply pending migrations to your development database:

$ bin/rails db:migrate

Rails keeps track of which migrations have already been run in the schema_migrations table. This command will only run migrations that haven’t been run yet.

Other useful migration commands:

  • bin/rails db:rollback: Reverts the last migration.
  • bin/rails db:rollback STEP=3: Reverts the last 3 migrations.
  • bin/rails db:migrate:status: Shows the status of all migrations (up or down).
  • bin/rails db:migrate VERSION=YYYYMMDDHHMMSS: Migrates up or down to a specific version.
  • bin/rails db:schema:load: Loads the schema from db/schema.rb into the current environment’s database (destructive, drops existing tables). Useful for setting up a new database quickly.
  • bin/rails db:seed: Runs the db/seeds.rb file.

Running Migrations for Different Environments

Section titled “Running Migrations for Different Environments”

To run migrations for a specific environment (e.g., production), set the RAILS_ENV variable:

$ RAILS_ENV=production bin/rails db:migrate

With your database schema defined and migrated, you can now focus on building the controllers and views that will allow users to interact with your application’s data.