I. Overview
Hi everyone, as a Rails developer you are no stranger to the concept of migration.
Migration in Rails allows us to develop database (database) throughout the life of an application. Migration allows us to write simple Ruby code to change the state of the database by providing elegant DSL. We don’t need to write database-specific SQL because migration provides abstractions to manipulate the database and take care of the details … when converting DSL into database-specific SQL queries. Migration also provides ways to execute raw SQL on the database, if required.
When creating an application with Rails it always generates a directory called db/migrate
, which contains all the migrations we will create.
For starters, we will create a rails app:
1 2 | rails new migration_demo |
Next, we will migrate a user
table with the column: name
into the database.
1 2 | rails g model User name:string |
The above command will create a file in the db/migrate
directory that contains the timestamp as follows: 20200521135455_create_users.rb
.
1 2 3 4 5 6 7 8 9 10 | class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :name t.timestamps end end end |
Let’s find out what this migration file looks like:
1. Timestamp in the file name:
- Every migration file created by Rails will have the timestamp in the file name.
- This timeline is very important and is used by Rails to confirm when a migration is running or not. I will go into more detail in this section later.
2. Rails version appears in superclass
- Migration contains a class that inherits from
ActiveRecord::Migration[6.0]
. As I am using Rails 6, the superclass migration contains[6.0]
. If you use rails 5.2, the superclass will beActiveRecord::Migration[5.2]
. We will discuss why the Rails version is part of the superclass name below.
3. change
method
- Migration has a
change
method that contains DSL code that manipulates the database. In the example above, thechange
method creates ausers
table with astring
name
column.
4. t.timestamps
- Migration uses the code
t.timestamps
to addcreated_at
andupdated_at
timelines to the database table.
When we run the migration using the rails db:migrate
command, it creates a users
table with the column name
with type type as string
and column created_at
, updated_at
with type as datetime
.
The actual database column type will be either varchar
or text
, depending on the database.
II. Detail
1. Importance of timestamps and schema_migration table.
1.1. timestamps
Every time a migration is generated by the rails g migration
command, Rails will generate the migration file with a unique timestamp. Timestamp in the form of YYYYMMDDHHMMSS
. When a migration runs, Rails inserts migration timestamp into schema_migrations table. This table was created by Rails when we first ran migraion. This table contains only the version
column, which is also the primary key. This is the architecture of the schema_migrations
table.
1 2 | CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY); |
Now that we run the migration to create the users
table, look below, if Rails contains the timestamp of this migration in the schema_migrations
table.
1 2 3 | sqlite> select * from schema_migrations; 20200405103635 |
If we re-run migrations. Rails will check to see if that timestamp already exists in the schema_migrations table, if it exists then it will not be executed. This ensures that we can increase the database changes over time and the migration will only run once on the database.
1.2. schema migration
When we run migrations many times. Database schema will continue to be expanded. Rails stores the latest database schema in the file db/schema.db
. This file represents all migrations that run on the database throughout the application life cycle.
Because of this file, we do not need to keep old migrations files in codebase. Rails provides the task to dump
final schema from database into schema.rb
and load
schema into database from schema.rb
. Therefore, older migrations can be safely removed from codebase. Loading schema into the database is also faster than running each migration every time we set up the application.
As we mentioned the verson of rails in the migation file, let me find out why.
2. Rails version in the migration file
Every migration we create has a version of Rails as part of a superclass. So if our application is using Rails 6, then of course our migration file will have the form ActiveRecord::Migration[6.0]
, if the application uses Rails 5.2 then it will take the form ActiveRecord::Migration[5.2]
,
If your application uses Rails 4.2 or lower, you will find that there is no version of Rails added in the migration file, it will look like this: ActiveRecord::Migration
The version of Rails has been added in migation since Rails 5. This basically ensures that the migration API can evolve over time without breaking the migrations generated by older versions of Rails.
To get a better understanding of this, let’s take a look at the same migration to create the users
table in Rails 4.2.
1 2 3 4 5 6 7 8 9 10 | class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :name t.timestamps null: false end end end |
If we look at the schema of the users
table created by Rails 6, we can see the NOT NULL
constraint for the timestamps column that exists.
1 2 3 | sqlite> .schema users CREATE TABLE IF NOT EXISTS "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL); |
This is because, starting with Rails 5 onwards, the migration API automatically adds a NOT NULL
constraint to the timestamps columns without adding it to the migration file. The version of Rails in the superclass name ensures that the migration uses the migration API of the Rails version where the migration process was created. This allows Rails to maintain compatibility, as opposed to older migrations, and develop the migation API.
3. change
method
The change
method is a main method in a migration. When the migration is run, it calls the change
method and executes the code inside it.
Along with create_table
, Rails also provides another method, change_table
. As the name suggests it is used to change the schema of an existing table.
1 2 3 4 5 6 7 8 | def change change_table :users do |t| t.remove :name t.string :email t.boolean :active, default: false end end |
In the example above, I removed the name
field and added 2 fields as email
string and active
boolean with default value of false
.
Rails also provides many other support methods that can be used such as:
add_column
remove_column
add_timestamps
rename_table
And some other methods you can find here
4. t.timestamps
TIMESTAMPS
We saw t.timestamps
added inside migration by Rails and created two columns, created_at
and updated_at
. These special columns are used by Rails to track when a record is created or updated. Rails adds values to these columns when the record is created and makes sure to update them when the record is updated. These columns help us track the lifetime of a record in the database.
Note: The updated_at column is not updated when we execute the update_all method in Rails. Because the update_all method will update by column rather than by row, it only updates the column we specify.
5. Revert migrations
Rails allows us to rollback the changes to the database with the following command:
1 2 | rails db:rollback |
This command reverts the last migration run to the database. As the above example shows, if migration has deleted the name
column, then after running this command, it will add that column again.
There is also another command to rollback the previous migration and run it: rails db:redo
.
Rails is smart enough to know how to reverse most migrations. But we can also provide suggestions for Rails to revert a migration using the up
and down
methods instead of using the change
method. The up
method will be used when the migration runs while the down
method will be used when the migration is rollback.
1 2 3 4 5 6 7 8 9 10 11 12 | def up change_table :users do |t| t.change :phone_number, :string end end def down change_table :users do |t| t.change :phone_number, :integer end end |
In the example above, we changed the phone_number
column from integer
to string
.
Similarly we can also write in the change
method as follows:
1 2 3 4 5 6 7 8 9 | def change reversible do |direction| change_table :users do |t| direction.up { t.change :phone_number, :string } direction.down { t.change :phone_number, :integer } end end end |
Rails also provides another way to revert the previous migration by using the revert
method as follows:
1 2 3 4 5 6 7 8 | def change revert CreateUsers create_table :users do ... end end |
The revert
method also accepts a block to revert a portion of the migration.
1 2 3 4 5 6 7 8 9 10 | def change revert do reversible do |direction| change_table :users do |t| direction.up { t.remove :name } direction.down { t.string :name } end end end |
III. Conclude
Above is my understanding of migration in Rails, hopefully helpful for you. And if you know more about migration then don’t forget to comment below so we can better understand migration. Thank you.
Reference link: https://guides.rubyonrails.org/active_record_migrations.html