While working in our application we need to make changes to the database. If we need to create a new table we can write like this.
1create table blogs(
2 id INT NOT NULL AUTO_INCREMENT,
3 title VARCHAR(100) NOT NULL,
4 body TEXT,
5 PRIMARY KEY ( id )
6);
In Rails we would write as shown below. This code gets the same thing done.
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 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.
Every time we run migration Rails writes the changed state of the database
to db/schema.rb
.
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.
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 ActiveRecord::Migration[6.0]
.
Here [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.
In the migration we have statement t.timestamps
.
That's a shortcut way of saying create two columns created_at
and 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_all
and update_column
will not update updated_at
value.
More on this will be covered in other chapters.
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.
Similarly, there is rails db:schema:dump
to take the current database status
and recreate db/schema.rb
.
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
We executed rails db:migrate
.
Then we realized that we need to change the column type
for body from string
to text
.
We have two choices.
Create another migration to change the type of body
.
We can also rollback the migration while we are still in development.
If we edit the migration without doing the rollback then when we execute
rake db:migrate
then the changed migration will not be executed since
Rails has already recorded that this migration has been executed.
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 20200530172019
is up
. It means if we run rails db:migrate
again then Rails
will ignore that migration file.
Now it's time to rollback this migration.
1bundle exec rails db:rollback
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 20200530172019
is down
. It means if we execute rails db:migrate
then this migration
will be executed.
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
Now we can run the migration.
1bundle exec rails db:migrate
It worked. However here is something to think about. When we rolled back the migration then what exactly happened.
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 change.
There are some special cases where Rails will not be able to decipher
what to do in case of a rollback.
In such cases we can help Rails by
having two methods up
and down
instead of change
.
That above 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
Sometimes we need to execute database specific command.
For such cases we can use execute
.
1class DeleteTasksTable < ActiveRecord::Migration[6.0]
2 def change
3 execute 'DELETE FROM tasks'
4 end
5end
Rails offers many tasks related to 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 manage the database. We will see some of these tasks in upcoming chapters.
Let's revert back the migration file to its original state.
1git checkout db/migrate/*
2git checkout db/schema.rb