Rails Migration — Part 2

Executing migration files with different migration tasks.

Prerequisite:

This article is a continuation of the previous article:

Following topics to be covered in this article

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 and books in database.
  • Generates db/schema.rb file (in first migration).
  • Generates db/development.sqlite3 file (in first migration if using SQLite database).

rake db:migrateor 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.

Viewing the database we created.

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! :)

Jr. Ruby on Rails Dev | Math Enthusiast | Ex-Machine Learning Engineer | https://juzershakir.github.io/