The Phonebook Database
Creating a database for out phonebook application is an essential task. It's an essential task for any application that needs to store data. We need to be able to create and recreate the database as the application evolves. Once you get that out of the way, you can focus on building your actual application. In a team setup, you need to make sure that everyone on the team has the same database schema. Changes made by one team member should be reflected in the database schema of all other team members. That's where migrations come in.
To create the database, we'll utilize the concept of migrations where the DDL (Data Definition Language) syntax is made up of small modular chunks that can be applied in sequence.
We'll use the gorm
package to create the migrations, while using the go-migrate
package to manage the migrations and keep track of the ones that have been applied.
Migration Structure
First, you'll need to create a new file in the pkg/db/migrations
folder.
You can name the file something like 01_create_contacts_table.go
to indicate that this migration creates the contacts
table.
Then copy the following code into that file.
Let's explain the migration code structure in detail:
- The
init
function is called when the package is imported. Keep in mind that Go compiler will import these files in alphabetical order, so the order of the migrations is important. - The
m
object holds a newMigration
object. - We set the
ID
of the migration, which is a unique identifier for the migration. This should naturally follow the name of the file. - The
m
object has two methods, theMigrate
and theRollback
functions is where we will define the changes to be made to the database schema. - Finally, we add the migration to the list of migrations using the
AddMigration
function.
All migrations will have the same structure as the above so you can bookmark this page for reference.
Migrations
func init() {
m := &gormigrate.Migration{}
m.ID = "yyyyMMdd01_migration_name"
m.Migrate = func(db *gorm.DB) error {
...
return nil
}
m.Rollback = func(db *gorm.DB) error {
...
return nil
}
AddMigration(m)
}
The general naming convention for migration files is yyyyMMdd01_migration_description.go
, where yyyyMMdd
is the date of the migration in the format year-month-day
, and 01
is the migration number as you may create multiple migrations on the same day.
It is acceptable to use omit the date and just use 01_migration_description.go
if your application is very small.
The Model
We'll cover the model in depth in the next tutorial, but for now, let's just define a simple model for our phonebook contacts.
This model will be used to create the plural contacts
table in the database.
Execution
The Migrate
function is where we define the changes to be made to the database schema.
The AutoMigrateAndLog
function is a helper that will automatically create the table if it doesn't exist, and log the applied migration ID.
It takes the database connection, the model to be migrated, and the migration ID as parameters.
And finally, it returns an error if the migration fails.
Separation of concerns
You could easily reference the Contact
model that we'll create in the next tutorial directly in the migration file, but it's a good practice to keep your migrations and models separate.
This way, you can change the model without affecting the migration, and vice versa.
For example, the Contact
model that's in the models folder may contain a derived Friends
field that is logically computed and populated at runtime, but that field is not relevant to the database schema.
Keeping models seprate from migrations allows you to track how your database schema evolves over time without affecting the application logic.
Another thing to note is that models do not implement relations and indexes in the database. You can however add those to your migrations.
Migration Model
...
type Contact struct {
models.ModelBase
FirstName string `gorm:"size:255"`
LastName string `gorm:"size:255"`
Nickname string `gorm:"size:255"`
Email string `gorm:"size:255"`
Phone string `gorm:"size:255"`
}
return AutoMigrateAndLog(db, &Contact{}, m.ID)
...
Rolling Back
The Rollback
function is used to revert the changes made by the Migrate
function.
You can use it to drop the table or remove specific columns or indexes that were added
by the migration.
Typically, any rollback function should be the inverse of the Migrate
function. here's a quick example of how to drop the contacts
table.
Rollback Migration
...
if err := db.Migrator().DropTable("contacts"); err != nil {
logFail(m.ID, err, true)
}
logSuccess(m.ID, true)
return nil
...
Danger!
Rolling back migrations can lead to data loss if not done carefully.
Make sure to back up your data before rolling back migrations.
Complete Code
Create Contacts Table Migration
func init() {
m := &gormigrate.Migration{}
m.ID = "01_create_contacts_table"
m.Migrate = func(db *gorm.DB) error {
type Contact struct {
models.ModelBase
FirstName string `gorm:"size:255"`
LastName string `gorm:"size:255"`
Nickname string `gorm:"size:255"`
Email string `gorm:"size:255"`
Phone string `gorm:"size:255"`
}
return AutoMigrateAndLog(db, &Contact{}, m.ID)
}
m.Rollback = func(db *gorm.DB) error {
if err := db.Migrator().DropTable("contacts"); err != nil {
logFail(m.ID, err, true)
}
logSuccess(m.ID, true)
return nil
}
AddMigration(m)
}
Applying migrations
Applying migrations means to create the actual database schema. This is generally something you do once, when you first set up your application or when you make changes to the database schema. To apply database migrations, all you have to do is run the following command:
Apply migrations
go run . db migrate