ActiveRecord Migrations
Still exploring Rails fundamentals and documenting my notes as I go. This one is on ActiveRecord migrations — how Rails lets you evolve…
ActiveRecord Migrations
Still exploring Rails fundamentals and documenting my notes as I go. This one is on ActiveRecord migrations — how Rails lets you evolve your database over time without hand-writing SQL.
Definition
It’s an ActiveRecord feature that lets you evolve your database schema over time. Instead of writing SQL schema modifications, ActiveRecord lets you describe changes to the schema using Ruby.
Think of each migration as a new version of the database — each one writes changes to the schema. And think of the schema as the structure of the database (its tables, columns, and indexes — not the data inside).
A migration file lives in the db/migrate folder, and Rails uses a convention where a timestamp is prefixed to the file name. This timestamp tells Rails which order to run migrations in.
The format is YYYYMMDDHHMMSS - Year, Month, Day, Hours, Minutes, Seconds.
Run the following command to generate a migration file:
rails generate migration CreateBooks
A file will be created in db/migrate matching the migration name:
# 20260605232747_create_books.rb
class CreateBooks < ActiveRecord::Migration[8.1]
def change
create_table :books do |t|
t.timestamps
end
end
end
Inside this block, define the attributes you want. In our case, we create a books table with a title:
class CreateBooks < ActiveRecord::Migration[8.1]
def change
create_table :books do |t|
t.string :title
t.timestamps
end
end
end
Run rails db:migrate and db/schema.rb will be updated to include the books table in the new version of the database.
Generating Migrations
1. Table generation
Format: Create<TableName>
rails generate migration CreateBooks
This results in what we saw before:
class CreateBooks < ActiveRecord::Migration[8.1]
def change
create_table :books do |t|
t.timestamps
end
end
end
2. Column creation
Format: Add<Column(s)>To<TableName> <column:type> <...rest>
rails generate migration AddTitleAndYearToBook title:string year:string
class AddTitleAndYearToBook < ActiveRecord::Migration[8.1]
def change
add_column :books, :title, :string
add_column :books, :year, :string
end
end
3. Column removal
Format: Remove<Column(s)>From<TableName> <column:type> <...rest>
Note: you need to specify the column type when removing a column, so the migration can be reversed in case of a rollback.
rails generate migration RemoveYearFromBook year:string
class RemoveYearFromBook < ActiveRecord::Migration[8.1]
def change
remove_column :books, :year, :string
end
end
4. Associations
There are mainly two use cases.
a) Establish a relationship between two tables by creating a foreign key reference in one of them.
rails g migration AddAuthorToBooks author:belongs_to
class AddAuthorToBooks < ActiveRecord::Migration[8.1]
def change
add_reference :books, :author, null: false, foreign_key: true
end
end
In schema.rb it added three things:
t.bigint "author_id", null: false
t.index ["author_id"], name: "index_books_on_author_id"
# ...rest
add_foreign_key "books", "authors"
author_id references the authors table's primary key (id).
Which brings us to: what is an index, and how is it used? An index in a book is a section that lists topics with the page numbers where they appear. Indexing a column creates a separate data structure (e.g. index_books_on_author_id) that keeps that column's values sorted, each pointing to its row - so the database can find matches without scanning the whole table.
b) Establish a join table between two tables. A join table stores foreign keys referencing two other tables — in the example below, both author_id and book_id.
You can also add two composite indexes for fast lookups in both directions. t.index [:author_id, :book_id] gives a fast lookup to find all books for an author, while t.index [:book_id, :author_id] gives a fast lookup to find the authors of a book.
rails g migration CreateJoinTableAuthorsBooks authors books
class CreateJoinTableAuthorsBooks < ActiveRecord::Migration[8.1]
def change
create_join_table :authors, :books do |t|
t.index [:author_id, :book_id]
t.index [:book_id, :author_id]
end
end
end
Migration Methods
1. Creating a table
create_table creates a table by the name provided and takes a block to describe its columns. The code below creates a products table with a name column:
create_table :products do |t|
t.string :name
end
By default it creates a primary key column called id. You can rename it with the :primary_key option:
create_table :products, primary_key: 'product_id' do |t|
t.string :name
end
To omit the primary key entirely, pass id: false:
create_table :products, id: false do |t|
t.string :name
end
2. Creating an association
Via create_table:
def change
create_table :products do |t|
t.belongs_to :category
end
end
This creates a category_id column, which we'll use to query for the category later:
product = Product.find(1)
# SELECT * FROM products WHERE id = 1 LIMIT 1;
category = product.category
# SELECT * FROM categories WHERE id = <product.category_id> LIMIT 1;
Or directly:
def change
add_reference :users, :role
end
Removing would be:
def change
remove_reference :products, :user
end
3. Creating a join table
create_join_table :products, :categories
This creates a join table named (by default) categories_products - the two names ordered alphabetically - with two columns, category_id and product_id. You can customize the name with the :table_name option:
create_join_table :products, :categories, table_name: :categorization
create_join_table also accepts a block to add indexes or additional columns:
create_join_table :products, :categories do |t|
t.index :product_id
t.index :category_id
end
4. Changing tables
change_table :products do |t|
t.remove :description, type: :string
t.string :title
t.index :part_number
t.rename :qrcode, :qr_code
end
5. Changing columns
change_column :books, :year, :integer
change_column_default :books, :published, from: true, to: false
change_column_null :books, :author_id, false
6. Reversible actions
When a migration runs forward it goes in the “up” direction; when you roll it back it goes “down”. For simple changes, Rails works out the down direction on its own. But some operations can’t be auto-reversed — change_column is the classic example, because Rails has no way of knowing the original type to restore. For those, you spell out both directions yourself.
Using reversible:
class ChangeBooksPrice < ActiveRecord::Migration[8.1]
def change
reversible do |direction|
direction.up { change_column :books, :price, :string }
direction.down { change_column :books, :price, :integer }
end
end
end
Separate up and down:
class ChangeBooksPrice < ActiveRecord::Migration[8.1]
def up
change_column :books, :price, :string
end
def down
change_column :books, :price, :integer
end
end
Both do the same job — they tell Rails exactly how to undo the change. Reach for reversible when you want everything in one change method, and for separate up/down when the two directions are different enough that splitting them reads more clearly.
Wrapping up
That’s the slice of the migration toolkit I reach for most: generating migrations, adding and removing columns, wiring up associations and join tables, changing tables and columns, and keeping it all reversible. Migrations are version control for your schema — treat each one as a small, reversible step and your database history stays easy to read and easy to roll back.
메타데이터
- post_id
- b48ebee00e6e
- slug
- activerecord-migrations-b48ebee00e6e
- url
- https://medium.com/@abdoamin/activerecord-migrations-b48ebee00e6e
- canonical_url
- https://medium.com/@abdoamin/activerecord-migrations-b48ebee00e6e
- author_url
- https://medium.com/@abdoamin
- status
- ok
- fetched_at
- 2026-06-16 19:09:56