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
Migratorinterface
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.
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
Afrom a model struct and add a fieldA2 - Rename a field
Afrom a model struct to fieldA2
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")
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.
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!