Rails Migration — Part 2
Executing migration files with different migration tasks.
Prerequisite:
This article is a continuation of the previous article:
Table of Contents
↦ Migration tasks
↪ rollback
↪ status
↪ VERSION
↪ down
↪ up
↪ STEP
↪ redo
↪ seed
↪ drop
↪ schema:load
↪ schema:dump
Executing Migration file:
Active Record provides several ways to run migration files, the migrations file we have are:
20210720050156_create_authors.rb
20210721053723_create_books.rb
The very first command we would probably run to create tables in the database is with:
rails db:migrate
or...
rake db:migrate
This runs the up
or the change
method for all the migration files that have not yet been run. db:migrate
task will execute these files in ascending order based on the timestamp in their file names.
Output:
== 20210720050156 CreateAuthors: migrating ====================================
-- create_table(:authors)
-> 0.0020s
== 20210720050156 CreateAuthors: migrated (0.0021s) ============================= 20210721053723 CreateBooks: migrating ======================================
-- create_table(:books)
-> 0.0023s
== 20210721053723 CreateBooks: migrated (0.0025s) =============================
After running the migration, the following files are created:
- creates table named
authors
andbooks
in database. - Generates
db/schema.rb
file (in first migration). - Generates
db/development.sqlite3
file (in first migration if using SQLite database).
rake db:migrate
or rails db:migrate
?
Commands like db:migrate
, db:reset
, db:test
etc which are part of rake library are also supported by rails after its release of version 5. So for Rails application which runs Rails version 5 or later, they can run migration commands with rake
or rails
. However, prior to version 5, the command rake
is the only option. More on this here.
Running the db:migrate
command invokes the db:schema:dump
task, which will create a schema.rb
file, which is a syntactical representation of our database, in the db/
directory. More on this is discussed in the Database Schema topic.
It also creates a development.sqlite3
file in db/
directory, which we will cover in the next topic.
Each migration is a new ‘version’ of the database.
View database
Assuming you’re using SQLite as a database for building rails applications, the migrations which you will run will create development.sqlite3
file, which is a visual representation of our database. To view it, you will need to download a software called SQLite browser for Ubuntu.
Migration Environments
By default, migrations will run in development
environment. To run the migrations in a different environment we specify RAILS_ENV
variable while running the migration task, like this:
rails db:migrate RAILS_ENV=test
This will run our migration in test
environment.
Which files migrate first?
As mentioned earlier, db:migrate
task will execute these files in ascending order based on the timestamp in their file names. So in our case, we have the following timestamp in the migration filename:
20210720050156
(authors)20210721053723
(books)
The authors' migration file was created on 20th July, It will run this migration first followed by the books migration file which was created later on 21st July.
Rails use migrations’ file numbers (the timestamp) to identify them. Active Record won't do anything if we run db:migrate
task on already executed migration files.
The combination of timestamps and recording which migrations have been run, allows Rails to handle common situations that occur with multiple developers.
Before Rails version 2.1, the migration number wasn’t a timestamp but a number that started from1
and incremented each time a migration file was generated after that. The issue with this approach was that if we had multiple developers working for a project, it was easy to clash with similar migration file names, requiring us to rollback migrations (will be discussed later) and re-number the files manually.
To overcome this, Rails introduced naming migration files with creation time when they were migrated. This made each migration file unique and hence avoided circumstances that developers faced before. However, we can revert to the old numbering scheme by adding the following line to config/application.rb
:
config.active_record.timestamped_migrations = false
Migration Tasks
The Active Record database allows us to migrate files in many ways. Following is a list of tasks that update the db/schema.rb
(schema file) to reflect the database.
rollback
There will be situations where we need to change/modify the table after we have run our migrations. Editing existing migration files and re-running db:migrate
won’t fix it because rails already know it has executed that migration file, hence db:migrate
won’t do anything.
To change or modify the table, first, we need to give db:rollback
, which is the opposite of db:migrate
, then edit our migration file and then re-run db:migrate
.
rails db:rollback
This will undo our last migration file, by running down
or change
method in the migration file. In our case, it will rollback 20210721053723_create_books.rb
file which will delete the books
table.
Editing an existing migration file is not a good idea especially if it is already running in a production environment. Instead, we should write a new migration that performs the changes we require to the table as needed.
However, editing a freshly generated migration file that has not yet been migrated is relatively harmless.
status
rails db:migrate:status
output:
database: ..db/development.sqlite3Status Migration ID Migration Name
--------------------------------------------------
up 20210720050156 Create authors
down 20210721053723 Create books
This comes in handy when we’re not sure which migration file has been executed. The up
status shows that db:migrate
has migrated that file while the down
status shows it hasn’t.
VERSION
It helps run a specific migration file whose status is down
. The value passed to VERSION
will be the timestamp of a migration file. In our case, we have books
migration file that we want to run:
rails db:migrate VERSION=20210721053723
Output:
== 20210721053723 CreateBooks: migrating ======================================
-- create_table(:books)
-> 0.0028s
== 20210721053723 CreateBooks: migrated (0.0040s) =============================
However, in our case, a better shortcut command is db:migrate
.
By default, db:migrate
runs up
method for our books
migration file. However, if we wanted to rollback a specific migration file we would give db:migrate:down
command.
down
rails db:migrate:down VERSION=20210721053723
Output:
== 20210721053723 CreateBooks: reverting ======================================
— drop_table(:books)
-> 0.0011s
== 20210721053723 CreateBooks: reverted (0.0026s) =============================
In our case, it deletes the books
table.
For our example, a better alternative shortcut would be db:rollback
.
up
It behaves opposite to the down
task, as discussed above, as creates book
table and updates the schema file:
rails db:migrate:up VERSION=20210721053723
For our example, 2 alternative methods are:
rails db:migrate VERSION=20210721053723
rails db:migrate
STEP
To undo multiple migration files, we provide a STEP
parameter:
rails db:rollback STEP=2
2
means it will undo the last 2 migration files. In our case, it would delete both tables, authors
& books
from database. 1
will undo the last migration file.
For rolling back multiple migrations, the STEP
task provides a more convenient way to migrate instead of providing multiple db:rollback
tasks.
redo
The redo
task is a combo of both db:rollback
& db:migrate
in one. And with it, we can give the STEP
task to migrate more than one migration file.
rails db:rollback:redo STEP=2
seed
rails db:seed
Loads the seed data into our database through the db/seeds.rb
file. This will execute only after all our migration files have been migrated. This however will not update the schema file as it feeds data to our table and doesn’t change the tables’ metadata.
Database Schema
A schema
starts with nothing in it, and with each migration, it modifies the metadata of our table or tables. Active Record knows how to update our schema
along this timeline, bringing it from whatever point it is in the history to the latest version. Alongside, Active Record will also update our db/schema.rb
file to match the up-to-date structure of your database.
A look at our db/schema.rb
file:
This file is created by inspecting the database and expressing its structure using methods like create_table
, change_table
, and so on. The db/schema.rb
file attempts to capture the current state of your database schema and is not designed to be edited. It's useful if we want to take a quick look at our database which would show information like how many and which tables are created, how many and what attributes each table has, etc. The information here is nicely summed up for us.
As this file is database-independent, it could be loaded into any database that Active Record supports, such as PostgreSQL, MySQL, etc where it could run against multiple databases.
In schema.rb
file, the version
key which is passed as an argument to a class method define
, has a value of UTC, the time at which schema file was last updated.
Types of schema dumps:
There are 2 ways to dump schema, either by SQL or Ruby and this value is set in config.active_record.schema_format
setting for which its value can either be :sql
or :ruby
in config/application.rb
file.
By default, its value is set to :ruby
and the schema is dumped in db/schema.rb
file which we saw above.
However, there are trade-offs using ruby as schema format, as it cannot express database-specific items such as foreign key constraints, triggers, or stored procedures. In a migration file, we can execute these custom SQL statements, however, the schema dumper cannot reconstitute those statements from the database. In such cases, we should set schema format to:sql
.
If :sql
is selected then the database structure will be dumped using a tool specific to that database via db:structure:dump
task into db/structure.sql
.
More Migration Tasks
drop
rails db:drop
This will delete our database file db/development.sqlite3
and db/test.sqlite3
(if it exists) without updating our schema file.
To re-create the database, we can either run db:migrate
which would run all the migration files that would re-create our table but won’t update our schema file version or we can use schema:load
task.
schema:load
It re-creates the database based on the schema file.
rails db:schema:load
This not only creates the database in development
but also in test
environment, as it creates 2 database files in db
directory, development.sqlite3
& test.sqlite3
.
schema:dump
It re-creates the schema file based on the database.
rails db:schema:dump
Assuming if our database is empty or it doesn’t exist, if we run the above task, our schema file would look like this:
ActiveRecord::Schema.define(version: 0) doend
..resets the version to 0.
version
rails db:version
Output:
Current version: 0
It outputs the current version of our schema file.
Normally, after running any migration file with any migration task which updates our schema file, the value of version
will be something like this: 2021_07_23_045128
which captures at what time our schema file was last updated.
Once you have created the tables in a database, how would you modify them? In part 3 I have covered this in-depth. See you there! :)