GORM, Short but Deep Dive

What is GORM?

GORM (Go Object-Relational Mapping) is an ORM library for Go that provides:

  • Abstraction: Maps Go structs to database tables
  • Query Building: Type-safe queries without writing raw SQL
  • Relationship Handling: Manages associations (has-one, has-many, many-to-many)
  • Hooks/Callbacks: Lifecycle events (BeforeSave, AfterCreate, etc.)
  • Transaction Management: Simple transaction handling

Key GORM Features

1. Model Definition

type Product struct {
    gorm.Model           // Embedded model (ID, CreatedAt, UpdatedAt, DeletedAt)
    Code  string         `gorm:"unique;size:100"`
    Price uint           `gorm:"default:0"`
    CategoryID uint      // Foreign key
    Category Category    // Belongs to relationship
}

2. CRUD Operations

// Create
db.Create(&user)

// Read
db.First(&user, 1)           // Find by ID
db.Where("age > ?", 18).Find(&users)

// Update
db.Model(&user).Update("name", "John")

// Delete (soft delete if DeletedAt exists)
db.Delete(&user)

3. Relationships

// One-to-Many
type User struct {
    ID       uint
    Orders   []Order    // User has many Orders
}

type Order struct {
    ID     uint
    UserID uint        // Foreign key
    User   User        // Order belongs to User
}

// Many-to-Many (like your example)
db.Model(&user).Association("Roles").Append(&role)

4. Hooks/Lifecycle

func (u *User) BeforeCreate(tx *gorm.DB) error {
    u.UUID = uuid.New() // Generate UUID before create
    return nil
}

func (u *User) AfterDelete(tx *gorm.DB) error {
    log.Printf("User %d deleted", u.ID)
    return nil
}

Migrations in Development Process

What are Migrations?

Migrations are version-controlled database schema changes that evolve alongside your codebase.

GORM Migration Approaches

1. Auto Migration (Development/Prototyping)

// Auto-create/update tables based on models
db.AutoMigrate(&User{}, &Role{}, &Product{})

// Pros:
// - Quick for development
// - Automatic schema updates
// Cons:
// - Limited control
// - Doesn't handle data migrations
// - Not for production changes

2. Manual Migration Files (Production-Ready)

# Migration file structure
migrations/
├── 20240101000000_create_users_table.go
├── 20240102000000_add_email_verification_to_users.go
└── 20240103000000_create_user_roles_table.go

Example migration file:

// migrations/20240101000000_create_users_table.go
package migrations

import (
    "gorm.io/gorm"
    "your_app/models"
)

func init() {
    migration := &gormigrate.Migration{
        ID: "20240101000000",
        Migrate: func(tx *gorm.DB) error {
            return tx.AutoMigrate(&models.User{})
        },
        Rollback: func(tx *gorm.DB) error {
            return tx.Migrator().DropTable("users")
        },
    }
    AddMigration(migration)
}

Development Workflow with Migrations

Phase 1: Initial Development

// main.go - During prototyping
func main() {
    db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})

    // Auto-migrate during development
    db.AutoMigrate(&User{}, &Role{})

    // Start development...
}

Phase 2: First Production Release

  1. Create initial migration:
# Generate SQL from models
gormt -H localhost -d mydb -u user -p password -o ./migrations/sql

# Or use migration tool
go install -u github.com/go-gormigrate/gormigrate/v2
  1. Create migration manager:
// cmd/migrate/main.go
package main

import (
    "log"
    "your_app/migrations"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    gormigrate "github.com/go-gormigrate/gormigrate/v2"
)

func main() {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
        // List all migrations in order
        migrations.CreateUsersTable,
        migrations.CreateRolesTable,
        migrations.CreateUserRolesTable,
    })

    if err := m.Migrate(); err != nil {
        log.Fatalf("Migration failed: %v", err)
    }
    log.Println("Migration complete")
}

Phase 3: Ongoing Development Cycle

Week 1: Add new feature requiring schema change

1. Create new model/model change:
   type User struct {
       // ... existing fields
       NewsletterSub bool `gorm:"default:false"` // NEW FIELD
   }

2. Create migration file:
   // 20240115000000_add_newsletter_to_users.go
   Migrate: func(tx *gorm.DB) error {
       return tx.Migrator().AddColumn(&User{}, "newsletter_sub")
   },
   Rollback: func(tx *gorm.DB) error {
       return tx.Migrator().DropColumn(&User{}, "newsletter_sub")
   }

3. Test migration:
   go run cmd/migrate/main.go  // Apply migration
   go run cmd/rollback/main.go // Rollback if issues

4. Commit migration file with feature code

Popular Migration Tools for GORM

1. GORMigrate (Most Popular)

import "github.com/go-gormigrate/gormigrate/v2"

m := gormigrate.New(db, gormigrate.DefaultOptions, migrations)
m.Migrate()  // Apply
m.Rollback() // Rollback last
m.RollbackTo("20240101000000") // Rollback to specific

2. Goose (SQL-based, language-agnostic)

# SQL files with up/down migrations
goose postgres "user=postgres dbname=mydb" up
goose postgres "user=postgres dbname=mydb" down

3. Atlas (Declarative, GitOps style)

# atlas.hcl
env "dev" {
  url = "postgres://localhost:5432/dev"
  migration {
    dir = "file://migrations"
  }
}

Best Practices for Migrations

1. Development Phase

// Use auto-migrate for rapid prototyping
func setupDevDB() *gorm.DB {
    db.AutoMigrate(&User{}, &Role{}) // Quick iteration
    db.AutoMigrate(&NewModel{})      // Add new models easily
}

2. Staging/Production Phase

// Use versioned migrations
func runMigrations() {
    // Each migration should be:
    // 1. Idempotent (can run multiple times)
    // 2. Reversible (has rollback)
    // 3. Tested in staging first
    // 4. Backed up before running
}

3. Migration File Naming Convention

YYYYMMDDHHMMSS_description.go
20240115143000_add_index_to_users_email.go
20240115143001_create_posts_table.go

4. Data Migrations Example

// When you need to transform data during schema change
Migrate: func(tx *gorm.DB) error {
    // 1. Add new column
    if err := tx.Migrator().AddColumn(&User{}, "full_name"); err != nil {
        return err
    }

    // 2. Migrate data from old to new
    tx.Exec("UPDATE users SET full_name = first_name || ' ' || last_name")

    // 3. Drop old columns
    return tx.Migrator().DropColumn(&User{}, "first_name", "last_name")
}

Common Migration Scenarios

1. Adding a Column

migrator.AddColumn(&User{}, "new_column")

2. Changing Column Type

migrator.AlterColumn(&User{}, "age") // GORM infers from struct

3. Adding Index

migrator.CreateIndex(&User{}, "email")
// or
db.Exec("CREATE INDEX idx_users_email ON users(email)")

4. Complex Migration with Transactions

Migrate: func(tx *gorm.DB) error {
    return tx.Transaction(func(tx *gorm.DB) error {
        // Multiple operations in one transaction
        if err := tx.AutoMigrate(&NewTable{}); err != nil {
            return err
        }
        if err := tx.Exec("UPDATE...").Error; err != nil {
            return err
        }
        return nil
    })
}

Migration Strategy Summary

ScenarioApproachTool
Early prototypingAutoMigratePure GORM
Small team, simple DBGORMigrateGORM + GORMigrate
Large team, complex DBSQL-based migrationsGoose, Flyway
Microservices, multiple DBsDeclarative migrationsAtlas

Your Code Example Context

Your code shows model definitions ready for:

  1. Initial migration: db.AutoMigrate(&User{}, &Role{})
  2. Many-to-many: GORM will create user_roles join table
  3. Soft deletes: Automatic filtering of deleted records
  4. Timestamps: Auto-managed created/updated times

For production, you’d create migration files for each model and relationship, ensuring version control and rollback capabilities.

Leave a Comment

Your email address will not be published. Required fields are marked *