GORM, Short but Deep Dive
2025-12-14

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
- 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
- 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
| Scenario | Approach | Tool |
|---|---|---|
| Early prototyping | AutoMigrate | Pure GORM |
| Small team, simple DB | GORMigrate | GORM + GORMigrate |
| Large team, complex DB | SQL-based migrations | Goose, Flyway |
| Microservices, multiple DBs | Declarative migrations | Atlas |
Your Code Example Context
Your code shows model definitions ready for:
- Initial migration:
db.AutoMigrate(&User{}, &Role{}) - Many-to-many: GORM will create
user_rolesjoin table - Soft deletes: Automatic filtering of deleted records
- Timestamps: Auto-managed created/updated times
For production, you’d create migration files for each model and relationship, ensuring version control and rollback capabilities.