What are Rails Migrations

A beginner friendly guide to Ruby on Rails migrations. What they are and why they are awesome, complete with examples and explanations.

Rails migrations are a tool that allows a developer to write Ruby code that will change an application’s database schema; that is, the tables and columns within the database.

In this article I’m going to try to explain more about what they are and why they are useful.

If you’ve not been writing Ruby on Rails that long, or are coming from a different framework that didn’t tie into database changes, this all might be quite new and complicated. Hopefully this article helps!

If you are a seasoned Rails developer, or have come from a framework that handles database changes in code. You may want to jump into the Rails guide on migrations to answer specific questions you have, you may find this article a bit light.

Lets start with an example, which is an actual migration I wrote earlier today, with the table name changed to avoid potentially giving the client away!

class AddHideDescriptionToBlogPosts < ActiveRecord::Migration[7.0]
  def change
    add_column :blog_posts, :hide_description, :boolean, default: false, null: false
  end
end

You don’t need to understand everything happening here, but to explain the main thing. We are saying when this migration is ran we want to add a column to the blog_posts table called hide_description. It will be a boolean, it will default to false, and we won’t allow null values (it either has to be true, or false, not empty).

If you’re curious about the ActiveRecord::Migration[7.0] part, I did some digging some time ago to understand what square brackets in Rails migrations are.

One more definition before I start the article, quoting from the Rails Migration Guide;

Migrations are a convenient way to alter your database schema over time in a consistent way. They use a Ruby DSL so that you don’t have to write SQL by hand, allowing your schema and changes to be database independent.

You can think of each migration as being a new ‘version’ of the database. A schema starts off with nothing in it, and each migration modifies it to add or remove tables, columns, or entries. Active Record knows how to update your schema along this timeline, bringing it from whatever point it is in the history to the latest version

Why use migrations?

The alternative to writing the migrations in our code is to make the changes directly on the database. There are several issues with this;

  1. Keeping track of what was changed and when for later troubleshooting can be complicated
  2. Sharing the changes with other team members, so they have the correct database can be a pain
  3. Rolling back changes if something goes wrong is easier with distinct migrations
  4. Getting the timings right, so database changes don’t break the application can be hard
  5. Changes don’t get committed into version control along with your code meaning there is extra work to get the full context of everything

I’ve worked on projects where database changes were not managed by the main application consuming the database. It is not, I repeat, not fun.

By reversal, migrations are great because;

  1. It is easy to keep track of changes
  2. It is quick to share changes with the team
  3. Rolling back changes is built in
  4. Migrations can happen at the point of code deploy
  5. Version control has a history of both code and data changes in one place

Why write migrations in Ruby

We write migrations in Ruby instead of writing SQL scripts. There are a few advantages to this;

  1. More Rails developers know Ruby than they do SQL
  2. Migrations are database independent, so you don’t need to know the specific SQL that differs between e.g. Postgres and MySQL
  3. You can use Ruby code to make the migrations cleaner, like create methods or use loops

Of course, sometimes there are advantages to writing something in SQL. Especially if you want to perform something not catered for by Rails. We won’t be covering that in this article, but Rails does have a mechanism for allowing raw SQL to be used.

What can I do with Rails migrations?

The most common things you can do with migrations are also the most common things you do when it comes to database work;

  • create, rename, delete tables
  • create, rename, edit, delete columns
  • create, rename, edit, delete indexes

Something I love about both Ruby and Rails are the names for things are often very clear. So rename_column, add_index, create_table are common methods we will see within migrations.

In theory you could also do things like add, edit, or remove data from the tables. I don’t think that is really in the spirit of what migrations are for and tend to avoid doing this, preferring instead to write a temporary rake task to manipulate data.

Having said that, sometimes you will need to update data. For example lets say we have a column that used to be optional but now it isn’t. We can’t just update the column to say null: false because there may be null entries already. In this case we would want to add some value to any null entries first.

Writing a Rails migration

You don’t tend to write the migration by hand, Rails has a built in tool to generate them for us.

A small note, the code in this article was written using Rails 7.0. Migrations have been part of Rails for a very long time, (maybe forever? They do seem to be in 1-2-stable, but don’t quote me!) and how you interact with them rarely changes. That being said, it is always good to double check the syntax for the version of Rails you are using.

This is the code I ran to start off the migration I shared at the start of this article.

rails generate migration AddHideDescriptionToBlogPosts hide_description:boolean

Once this runs a new file will be created db/migrate/20240220110925_add_hide_description_to_blog_posts.rb;

class AddHideDescriptionToBlogPosts < ActiveRecord::Migration[7.0]
  def change
    add_column :blog_posts, :hide_description, :boolean
  end
end

I made a small modification to this file to add the default: false and null: false.

class AddHideDescriptionToBlogPosts < ActiveRecord::Migration[7.0]
  def change
    add_column :blog_posts, :hide_description, :boolean, default: false, null: false
  end
end

There are other options you can pass in, for example you can comment tables and fields in Rails.

Once you’re happy with your migration, you can tell Rails to apply it to your database with;

rails db:migrate

This command will run any migration that hasn’t yet been ran and report back on the success of each migration.

If you’re wondering how it knows which migrations to run; Look at the filename of your migration and note there is a series of numbers. This is a timestamp. Anytime a migration is successfully ran Rails will keep a note of the time in your database. Running rails db:migrate will only look for files with a larger timestamp.

Rails migration errors

Sometimes a migration will fail, or will work but you realise you wanted it to do something else. This is where rollback comes in.

rails db:rollback

The rollback command will undo the last migration. Now the definition of undo will differ between migrations. In our example because we were doing an add_column, it will remove the column.

It is good practice to make sure that when feasible, migrations can be undone. Which is why you will often see that when developers are deleting a column, they will specify the data needed to remake that column should you want to undo it.

If you delete a column on a table, even if you recreate that column again, the data is gone. Take your time understanding the impact of your changes on your local and test environments before deploying them to production.

Speaking of production…

Deploying your migrations

We use the same migrations on production as we do locally and on test servers. Specifically how you do that will depend on how your application is hosted.

You generally want migrations to happen at the point the new code is ready to take advantage of the latest database changes.

If you use Heroku, we have written a guide to running Rails migrations automatically on Heroku.

The schema.rb file

You will notice once you run a migration, that a file called db/schema.rb gets updated. This file is a representation of your database at the point of the last migration.

schema.rb is useful for quickly understanding what your database should be doing. It can also be used in other Rails commands to quickly get a database up and running, for example with rails db:schema:load.

We’re already at over 1400 words on this post, so I won’t get into that here!

Tips for learning migrations

The best way to learn migrations is to write a lot of them.

Over time you will get better at remembering the rails generate migration command and writing things in a rollbackable way (definitely a word!). Of course if you’re working on a project that has a fairly stable database, you might not get the chance to write lots of them.

Luckily, because migrations get committed with the rest of the codebase, you can view any open source Rails projects and see how their migrations are written. This will help show you how other developers have organised their migrations.

Here is the migration folder for the mastodon social network project, for example.

This article is a part of the "Rails migrations" series


Recent posts View all

Web Dev

Creating draft posts in Jekyll

How to create and develop with draft posts in Jekyll

Ruby

Forcing a Rails database column to be not null

How you can force a table column to always have something in it with Rails