10 minutes read

Today we continue learning about GORM. In this topic, you'll learn how to generate a database schema with GORM models, as well as how to migrate the newly created schema to a SQLite database using GORM.

What are migrations?

GORM allows you to execute migrations to propagate changes you make to the database, such as creating a new schema or adding new models to an existing schema. It also allows you to modify existing models, such as adding a new field or deleting an existing model.

One of the benefits of using an ORM library such as GORM is that you don't need to know any fancy SQL code to create a database with tables in it. You can describe to GORM the models you want to make, and after executing the first migration, it will automatically create the models within the database.

Types of migrations

GORM allows you to run two different types of migrations:

  • Automatic migrations using the db.AutoMigrate() method
  • Single migrations using the methods implemented by GORM's Migrator interface

The simplest way to run migrations with GORM is to use the db.AutoMigrate() method; it allows you to automatically sync the state of the database schema with the state of the model structs declared in your code.

When using db.AutoMigrate() GORM automatically creates tables, missing foreign keys, constraints, columns, and indexes. It will change the existing column's type if its size, precision, or nullable has changed. However, it won't delete unused columns to protect the data within the tables.

Since db.AutoMigrate() is agnostic to deleting and renaming columns, it cannot distinguish between these situations:

  • Delete a field A from a model struct and add a field A2
  • Rename a field A from a model struct to field A2

In both cases, the column A would remain in the table, and a new column A2 would get added to the table instead.

Another important detail is that you can't use db.AutoMigrate() to delete or rename tables in the database. So any operations that involve deleting/renaming columns or tables should be called explicitly from your code via single migrations using methods from GORM's Migrator interface.

To sum up, below is a table with the operations that each type of migration can perform:

Operation Auto Migration Single Migration
Create a table
Delete a table
Rename a table
Add a column
Rename a column
Delete a column
Modify a column data type

Running the first migration

Before running the first migration, you need to declare a model struct to be migrated. Let's go ahead and declare the Employee model:

type Employee struct {
    gorm.Model
    FirstName string
    LastName  string
    Salary    int
}

After declaring the Employee model, it's time to use db.AutoMigrate() to run the first migration for the company SQLite database:

... // `Employee` Model declaration goes here

func main() {
    // Connect to the `company` SQLite database
    // For sqlite3 databases, if the DB file does not exist, gorm.Open() will create it:
    db, err := gorm.Open(sqlite.Open("company.db"), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    // Auto Migrate the `Employee` model and create the `employees` table:
    err = db.AutoMigrate(&Employee{})
    if err != nil {
        log.Fatal(err)
    }
}

The db.AutoMigrate() method takes as arguments a variadic number of pointers to model structs, and if the migration fails, it returns an error.

After executing the above code, the company.db sqlite3 file will be created within your project workspace with the employees table.

Note that you could also create the employees table with a single migration using the CreateTable() method from the Migrator interface:

// Create the `employees` table with a single migration:
if err = db.Migrator().CreateTable(&Employee{}); err != nil {
    log.Fatal(err)
}

Running single migrations

You already know how to use GORM to create tables; it's time to learn how to modify them. Let's start by renaming the salary column to remunerations, and add a new column birthday via single migrations.

The first step is to modify the fields within the Employee struct; then, you can use the AddColumn() and RenameColumn() methods from GORM'sMigrator interface to perform the above operations:

type Employee struct {
    gorm.Model
    FirstName    string
    LastName     string
    Remuneration int       // 1. Rename the `Salary` field to `Remuneration`
    Birthday     time.Time // 2. Add a new field `Birthday`
}

func main() {
    ... // 3. Connect to the `company` SQLite database

    // 4. Add a new column `birthday`:
    err = db.Migrator().AddColumn(&Employee{}, "Birthday")
    if err != nil {
        log.Fatal(err)
    }

    // 5. Rename the `salary` column to `remuneration`:
    err = db.Migrator().RenameColumn(&Employee{}, "Salary", "Remuneration")
    if err != nil {
        log.Fatal(err)
    }
}

Most of the Migrator interface methods return an error in case they fail, so make sure to add error handling each time you use them!

Note that you can also pass as arguments column names in snake_case to the Migrator interface methods, for example:

// Rename the `first_name` column to `name`:
db.Migrator().RenameColumn(&Employee{}, "first_name", "name")

And if you wanted to delete a specific column, you would first remove the field from the model struct declaration and then use the DropColumn() method via the following syntax:

... // Remove the `LastName` field from the `Employee` model declaration

// Delete the `last_name` column:
db.Migrator().DropColumn(&Employee{}, "LastName")

When working with SQLite databases, if you use the AlterColumn() or DropColumn() methods, GORM will create a new temporal table as the one you're trying to change, copy all data from the original table to the new table, drop the original table, and finally rename the new table to the original table name.

This new table will NOT have any indexes! So after performing any modification or deletion operation, you should run another independent migration with db.AutoMigrate() to create table indexes.

There are many other methods to run single migrations in GORM's Migrator interface; you can look at them in GORM's official documentation.

When to run migrations?

Now you might be wondering, when should we run migrations during the execution of a Go program? It's preferred to run automatic migrations as often as you have changes in your database schema or model struct declarations.

Usually, the first migration is executed using db.AutoMigrate() at the start of your Go program. And if you execute db.AutoMigrate() once again after you've already run the first migration and generated the DB schema, nothing would happen unless you made changes to your model structs.

In short, running db.AutoMigrate() every time you start your application is safe unless you made changes to the DB schema or your model structs.

A special case is if you are working with the in-memory :memory: SQLite database that gets deleted after the program ends. In this case, you would have to run db.AutoMigrate() at the start of your program every time to generate the DB schema.

In the case of single migrations, you should only run them once; otherwise, you may get an error. For example, if you use RenameColumn() to rename a column and then use it to rename the same column again, you would get an error.

You can take a look at logs of SQL operations GORM performs in your terminal by using the db.Debug() method:
db.Debug().AutoMigrate(&Employee{})
db.Debug().Migrator().RenameColumn(&Employee{}, "Salary", "Remuneration")

Finally, an elegant solution to deal with single migrations is to create an independent Go program that takes care of the single migration operation you want to perform. For example, rename_column_salary_remuneration.go — that way, you can keep the main application's code unrelated to the execution of single migrations!

Conclusion

Understanding how migrations work is a crucial part of the learning process about any ORM library since they allow you to propagate changes to the database schema without having to write any SQL code.

Now it's time to test your knowledge about migrations with a few theory and coding tasks; let's go!

10 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo