Migrations are most commonly used for database schema changes like creating or changing tables and columns, as they change the data stored in the database. They modify database schemas over time whenever it is necessary to update or revert that database’s schema to some newer or older version. Migrations are performed programmatically by using a schema migration tool, and they usually keep information about the migrations themselves in a dedicated database table. Different programming languages use different schema migration tools. By using migrations, developers can apply these changes to the database schema consistently and repeatedly, ensuring that all instances of the application use the same schema version. 

One of the most commonly used database migration tools for Elixir is Ecto. Ecto migrations can be used to perform the following changes:

  • Change the structure of the database, such as adding fields, tables, indexes, constraints, and so on
  • Populating tables with new or modified data
  • Rename tables, columns, or indexes

Getting started

After creating our first Elixir application, we have to add Ecto dependency to this application. Besides adding Ecto, we have to add a database driver also. Since Postgres is a widely represented and used database, we will add support to Postgres driver in this example. First, we have to locate our mix.exs file and change the deps definition. This function defines all dependencies inside our application. We need to introduce changes to this function, such as:

defp deps do
  [
    {:ecto_sql, "~> 3.8"},
    {:postgrex, ">= 0.0.0"}
  ]
end

Once we add new dependencies to our application, it is necessary to install them. This can be accomplished by executing the following command:

mix deps.get

After installing our dependencies for Ecto and Postgres, we can continue setting up and creating our database. For this tech bite, we will assume that our database has already been created and is prepared for migrations, so we will not cover that aspect.

Creating migrations

For managing migrations, Ecto creates a special table inside our database called schema_migrationsThis table contains all migrations that have already been executed. Ecto allows us to configure the name of this table using :migration_source configuration option. While performing a migration, Ecto locks schema_migrations table, guaranteeing two different servers cannot run the same migration at the same time.

All migrations are defined inside the priv/REPO/migrations, where REPO is the last part of the repository name in underscore. For example, if we have a project named MyApplication.TestRepomigrations for it would be found in priv/test_repo/migrations folder.

When creating migration files, there are certain naming conventions we should follow. Each migration file should be named like <timestamp>_<migration_name>.exs. The timestamp part is a unique number that identifies the migration. This is usually the Unix timestamp of when the migration is created. The migration_name usually identifies what the migration does, meaning it’s a descriptive name for the migration. For example, suppose we have a project for maintaining users, their profiles and information. In that case, we can create a migration file that adds table user to the existing database under the name 20230402134000_add_user_table.exs with the following content:

defmodule MyApplication.Repo.Migrations.AddUserTable do
  use Ecto.Migration
  
  def up do
    create table("user") do
      add(:first_name, :string)
      add(:last_name, :string)
      add(:user_name, :string)
      add(:address, :string)
      add(:email_address, :string)
      add(:inserted_at, :utc_datetime)
      end
  end
 
  def down do
    drop table("user") 
  end
end    

The up/0 function is responsible for migrating our database forward, meaning it defines the changes to be made to the database schema when the migration is run, while down/0 is triggered whenever we want to rollback changes. The down/0 function in Ecto migrations is designed to reverse the changes made by the up/0 function, ensuring that it always does the opposite of the up/0 function. In our example, up/0, creates table user, while down/0 drops it.

To successfully run the migration, we can simply call Mix tasks. In order to execute our migration, we must navigate to the root of the project and run the following command mix ecto.migrate.

When a migration is run, Ecto checks the current version of the database schema and applies the necessary changes to bring the schema up to date. If migration is successful, a new table under the name user will be created with corresponding fields. If some error occurs or we want to change the migration, we can rollback the changes that we introduced with the latest migration. In order to achieve this, from the root of the project, we must execute mix ecto.rollback –step 1. Note that we must say how many migrations we want to rollback. In this case, we only rollback one migration. If we want to rollback all migrations, we simply execute mix ecto.rollback. It’s important to say that mix ecto.migrate will always run all pending migrations.

Usually, in practice, we don’t create these files manually, rather we use mix ecto.gen.migration command and let Ecto create a file with a proper timestamp and later, we write our migration. Using this approach, we can create our migration file in the following way:

mix ecto.gen.migration add_user_table

This will create an empty migration file with the proper timestamp and name add_user_table, where we simply write our migration code.

Instead of using up/0 and down/0 functions, which can cause confusion and error since we have to write both functions, Ecto offers another function for migrations, and that’s change/0. When using change/0, we only have to write the code we want to execute when migrating, and Ecto will automatically write a rollback down/0 function in the background. Using this approach, previous migration can be written the following way:

defmodule MyApplication.Repo.Migrations.AddUserTable do
  use Ecto.Migration
  
  def change do
    create table("user") do
      add(:first_name, :string)
      add(:last_name, :string)
      add(:user_name, :string)
      add(:address, :string)
      add(:email_address, :string)
      add(:inserted_at, :utc_datetime)
      end
  end
end    

In case we have implemented both up/0, down/0, and change/0 in our migration, change/0 won’t be invoked since up/0 and down/0 have higher priority. If we want to see logs while executing migrations, we can call mix ecto.migrate but with an additional flag mix ecto.migrate –log-sql or (–log-migrations-sql in later versions). When a migration fails, the transaction is rolled back and no modifications are made to the database.

Field types

Ecto uses primitive types for mapping between migrations and Elixir data types. Some examples are :string is mapped to varchar, :binary to blob and so on. One more thing worth mentioning is that :string is limited to 255 characters by default in Ecto migrations. If we need more or less characters, we can pass an extra flag :option which dictates the size of :string field (example is:  add(:first_name, :string , size: 100)).

Types that have space in their name, are wrapped in double quotes, for example add(:id, :“int unsigned”) or add(:created_at, “time without time zone”).

Conclusion

Using Ecto migrations brings many advantages such as:

  • We keep our code clean and organized
  • Each migration is only run once, unless we roll it back
  • All migrations run in order and we have a history of all performed migrations
  • Ecto ensures only one node will run migration at a time
  • We can configure it to run automatically on deployment so we don’t have to worry about that part
  • Migrations provide a way to roll back changes to the database schema, allowing developers to undo changes if needed easily

Using migrations, we can track how our database evolves and changes. Overall, Ecto migrations are an essential tool for managing database schema changes in Elixir applications, providing a structured and reliable way to evolve the schema as the application progresses over time while maintaining data integrity.


“Elixir Migrations using Ecto” Tech Bite was brought to you by Alma Ibrašimović, Software Engineer at Atlantbh.

Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.

Leave a Reply