Introduction
This guide demonstrates how to set up and use GORM with Golang Migrate to manage your PostgreSQL database. Follow along to see the code in action and get your environment running.
Prerequisites
- Go installed on your system.
- Docker installed and running.
Project Structure
Here is the final directory structure:
.
├── .env
├── Makefile
├── cmd
│ └── cli
│ └── main.go
├── docker-compose.yml
├── go.mod
├── go.sum
└── internal
├── db
│ └── migrations
│ ├── 20240608192206_create_users_table.down.sql
│ └── 20240608192206_create_users_table.up.sql
├── models
│ └── models.go
└── repositories
└── repositories.go
8 directories, 10 files
Step 1: Initialize Your Project
1. Initialize the Go module:
go mod init github.com/yourusername/yourproject
2. Install dependencies:
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/golang-migrate/migrate/v4
go get -u github.com/golang-migrate/migrate/v4/database/postgres
go get -u github.com/golang-migrate/migrate/v4/source/file
go get -u github.com/joho/godotenv
3. Set up Docker Compose:
# docker-compose.yml
services:
gorm-db-post:
image: postgres:16
restart: always
ports:
- "5433:5432"
env_file:
- .env
4.Create the .env file:
# .env
POSTGRES_DB=mydb
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_HOST=localhost
POSTGRES_PORT=5433
Step 2: Define Your Models
// internal/models/models.go
package models
import (
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string
Email string `gorm:"uniqueIndex"`
}
type UserCreateRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
Step 3: Set Up Repositories
// internal/repositories/repositories.go
package repositories
import (
"context"
"github.com/yourusername/yourproject/internal/models"
"gorm.io/gorm"
)
func New(db *gorm.DB) *Repositories {
return &Repositories{
User: UserRepository{db: db},
}
}
type Repositories struct {
User UserRepository
}
type UserRepository struct {
db *gorm.DB
}
func (u *UserRepository) Create(ctx context.Context, params models.UserCreateRequest) (*models.User, error) {
user := models.User{
Name: params.Name,
Email: params.Email,
}
if err := u.db.WithContext(ctx).Create(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
Step 4: Leverage Makefile to write migration files
1. Install golang-migrate
If you are on MacOS, you can use brew
:
brew install golang-migrate
For other OS’s, follow the instructions here: https://github.com/golang-migrate/migrate/tree/master/cmd/migrate
2.Create a Makefile to run migrate tool
Use the following Makefile
to help you generate migrations:
include .env
$(eval export $(shell sed -ne 's/ *#.*$$//; /./ s/=.*$$// p' .env))
DOCKER_COMPOSE_FILE := docker-compose.yml
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)
## Development
dc-up: ## Start the docker-compose services
@echo "${GREEN}Starting docker-compose services...${RESET}"
@docker-compose -f $(DOCKER_COMPOSE_FILE) up -d
dc-down: ## Stop the docker-compose services
@echo "${GREEN}Stopping docker-compose services...${RESET}"
@docker-compose -f $(DOCKER_COMPOSE_FILE) down
## Database
db-create-migration: ## TO use pass the name as `make create-migration name=your_migration_name`
@echo "${GREEN}Creating migration...${RESET}"
@migrate create -dir internal/db/migrations -ext sql -digits 6 $(name)
## Help:
help: ## Show this help.
@echo ''
@echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target>${RESET}'
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)
If you run make help
, you will see:
Usage:
make <target>
Targets:
Development
dc-up Start the docker-compose services
dc-down Stop the docker-compose services
Database
db-create-migration TO use pass the name as `make create-migration name=your_migration_name`
Help:
help Show this help.
3.Create migrations
make db-create-migration name=create_users_table
4.Fill migration files
Migration up file:
-- internal/db/migrations/{date}_create_users_table.up.sql
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP
);
Migration down file:
-- internal/db/migrations/{date}_create_users_table.down.sql
DROP TABLE IF EXISTS users;
Step 5: Putting it all together
1. Create the main.go
file inside cmd/cli/main.go
:
// cmd/cli/main.go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/hyeomans/gorm-migrate-blogpost/internal/models"
"github.com/hyeomans/gorm-migrate-blogpost/internal/repositories"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
ctx := context.Background()
// Load environment variables
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
dbUser := os.Getenv("POSTGRES_USER")
dbPassword := os.Getenv("POSTGRES_PASSWORD")
dbName := os.Getenv("POSTGRES_DB")
dbHost := os.Getenv("POSTGRES_HOST")
dbPort := os.Getenv("POSTGRES_PORT")
dbUrl := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dbUser, dbPassword, dbHost, dbPort, dbName)
m, err := migrate.New(
"file://internal/db/migrations",
dbUrl,
)
if err != nil {
log.Fatalf("failed to create migrate instance: %v", err)
}
handleMigrateUp(m)
db, err := gorm.Open(postgres.Open(dbUrl), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
// Close the database connection
repo := repositories.New(db)
// Create a user
userReq := models.UserCreateRequest{
Name: "John Doe",
Email: "[email protected]",
}
user, err := repo.User.Create(ctx, userReq)
if err != nil {
log.Fatalf("failed to create user: %v", err)
}
log.Printf("created user: %v", user)
}
func handleMigrateUp(m *migrate.Migrate) {
if err := m.Up(); err != nil {
if err.Error() == "no change" {
log.Println("no change")
return
}
log.Fatalf("failed to apply migration: %v", err)
}
}
2. Start Postgresql:
In a terminal window, run:
make dc-up
3. Run the application:
go run cmd/cli/main.go
Conclusion
By using GORM and Golang Migrate, you can manage your database schema and interact with your PostgreSQL database efficiently. This setup helps maintain versioned migration files and provides better control over database changes.
Follow-up
- How do you handle migrations in a production environment with Golang Migrate?
- What are some best practices for structuring your models and repositories in a Golang application?
- How can you extend this setup to include more complex relationships and associations between models?