What is Rails migration
While introducing new features to our application, we will most probably need to make changes to the structure of our database. This would include creating new tables, adding columns to existing tables, feeding additional data to the database, etc.
If we were using plain SQL, we can create a new table on an SQLite database like this:
1create table blogs( 2 id integer PRIMARY KEY AUTOINCREMENT NOT NULL, 3 title VARCHAR NOT NULL, 4 body TEXT, 5 created_at datetime NOT NULL, 6 updated_at datetime NOT NULL 7);
In Rails we would write the same like this:
1create_table :blogs do |t| 2 t.string :title 3 t.text :body 4 t.timestamps 5end
There are several advantages of writing migrations in Ruby. The most important ones are:
- The migration code in Ruby is database independent. We can run the same code for SQLite3, PostgreSQL and for MySQL and it will work.
- All migrations run sequentially and they are additive. Each migration adds to the previous state of the database.
schema.rb file is the final source of truth
Every time we run migration Rails writes the changed state of the database to
If we open
db/schema.rb file then the very first three lines says this.
1# This file is auto-generated from the current state of the database. Instead 2# of editing this file, please use the migrations feature of Active Record to 3# incrementally modify your database, and then regenerate this schema definition.
Rails version in the migration file
If we look at the last migration file it looks like this.
1# 20200523233758_create_tasks.rb 2class CreateTasks < ActiveRecord::Migration[6.0] 3 def change 4 create_table :tasks do |t| 5 t.text :title 6 t.timestamps 7 end 8 end 9end
As we can see our class is inheriting from
[6.0] is the Rails version used to create the migration.
Note that the filename starts with a timestamp. Rails uses this timestamp to determine if that migration has already been executed or not.
Rails executes a migration only one time. It means once a migration has been executed then any changes made to that migration will have no impact unless we "rollback" that migration.
Timestamp metadata in migrations
In the migration we have statement
t.timestamps. That's a shortcut way of
saying create two columns
updated_at. Rails encourages us to
have these two columns in all the tables.
Whenever a new record is created then
created_at is automatically populated by
Rails. Similarly, whenever a record is updated then
updated_at column is
updated with when the record was updated.
Some methods like
update_column will not update
value. More on this will be covered in the coming chapters.
Load schema and dump schema
In a large application which has been in development for a few years the number
of migrations can be 100 or even more. In such cases when we run
rake db:migrate then Rails executes each migration one by one sequentially.
This could take some time.
A faster way is to execute
rails db:schema:load. This skips all the migrations
and directly executes the
db/schema.rb and changes the database. But the
downside is that this method only restores the database structure. This won't
fill the database with essential data that the migration script would have added
Similarly, there is
rails db:schema:dump to take the current database status
Let's say that we have a migration like this.
1class CreateTasks < ActiveRecord::Migration[6.0] 2 def change 3 create_table :tasks do |t| 4 t.string :body 5 t.timestamps 6 end 7 end 8end
rails db:migrate. Then we realized that we need to change the
column type for body from
We have two choices. Create another migration to change the type of
rollback the migration, edit it, and then re-run it.
If we edit the migration and re-run
rails db:migrate without rolling back, the
changes won't reflect in the database. This is because rails marks a migration
file as done once it is executed and won't execute it again.
To rollback and re-run, first, let's ask Rails what is the migration status:
1bundle exec rails db:migrate:status
1database: db/development.SQLite3 2 3 Status Migration ID Migration Name 4-------------------------------------------------- 5 up 20200530172019 Create tasks
As we can see above the status of the migration ID
means if we run
rails db:migrate again then Rails will ignore that migration
Now, to rollback this migration, run this command:
1bundle exec rails db:rollback
Check the status again:
1bundle exec rails db:migrate:status
1database: db/development.SQLite3 2 3 Status Migration ID Migration Name 4-------------------------------------------------- 5 down 20200530172019 Create tasks
After the rollback, we can see that the status of migration ID
down. It means if we execute
rails db:migrate then this migration will be
Now, we can change the migration file. The changed file looks like this:
1class CreateTasks < ActiveRecord::Migration[6.0] 2 def change 3 create_table :tasks do |t| 4 t.text :body 5 t.timestamps 6 end 7 end 8end
Run the migration again:
1bundle exec rails db:migrate
It should work!
But what exactly happened when we rolled back the migration?
Notice that the method name in the migration is
change. When we execute
migration then Rails adds that change. When we rollback then Rails removes that
Operations like adding tables and columns are reversible. Rails can reverse them by dropping the added table or column. In that case, the database will be in the exact same state as it was before.
There are some other cases where Rails will not be able to reverse the changes. For example: dropping a column. In that case, since Rails doesn't know what all data the column contained before, the database cannot be reverted to its previous state.
In such cases we can help Rails by having two methods
down instead of
change. Here we can manually specify what to do when the migration is being
Even though it is redundant, our previous migration could also be written like this:
1class CreateTasks < ActiveRecord::Migration[6.0] 2 def up 3 create_table :tasks do |t| 4 t.string :body 5 t.timestamps 6 end 7 end 8 9 def down 10 drop_table :tasks 11 end 12end
Raw execution of sql statements
Sometimes we need to execute database specific command. For such cases we can
1class DeleteTasksTable < ActiveRecord::Migration[6.0] 2 def change 3 execute 'DELETE FROM tasks' 4 end 5end
But keep in mind that SQL syntax and functions vary with the database engine. So if you are working on different databases for development (eg: SQLite) and production (eg: PostgreSQL), writing native SQL will be risky.
Also note that even if the SQL we supplied is practically reversible, Rails
isn't intelligent enough to find a correct rollback strategy for SQL statements.
Therefore, we will need to manually supply
down methods for the
migration script to be able to be rolled back.
More tasks from Rails to manage database
Rails offers many tasks related to the database management.
1bundle exec rails -T db
1rails db:create 2rails db:drop 3rails db:environment:set 4rails db:fixtures:load 5rails db:migrate 6rails db:migrate:status 7rails db:prepare 8rails db:rollback 9rails db:schema:cache:clear 10rails db:schema:cache:dump 11rails db:schema:dump 12rails db:schema:load 13rails db:seed 14rails db:seed:replant 15rails db:setup 16rails db:structure:dump 17rails db:structure:load 18rails db:version 19rails test:db
As we can see, there are a lot of rake tasks to manage the database. We will see some of these tasks in detail in the upcoming chapters.
The changes we made to the files were for demonstration only. Let's revert the changes.
1git clean -fd
Some of the changes we have made to our database are irreversible. To get it back in our old state, let us delete it, create a new one, and then re-run the migration scripts on it.
Run this command to do so:
1bundle exec rails db:reset