---
title: "Mastering GORM: Go's Powerful ORM"
url: "https://vanducng.dev/2024/03/15/Mastering-GORM-Go-Powerful-ORM/"
date: "2024-03-15"
series: "go-backend"
categories: ["Backend"]
tags: ["go","postgresql"]
---

# Mastering GORM: Go's Powerful ORM

GORM is Go's most popular Object-Relational Mapping (ORM) library, providing an elegant way to work with databases using Go structs. Instead of writing raw SQL, GORM lets you interact with database records as Go objects, handling the translation between your code and the database automatically.

## Why Use an ORM?

The fundamental challenge: bridging two worlds.

```mermaid
flowchart LR
    subgraph GoWorld["Go World"]
        S[Struct]
        M[Methods]
        T[Types]
    end

    subgraph DBWorld["Database World"]
        TB[Tables]
        R[Rows]
        C[Columns]
    end

    GoWorld <-->|ORM| DBWorld

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff

    class GoWorld blueClass
    class DBWorld greenClass
```

Without an ORM, you'd write the same data definition twice:

```go
// Go struct
type Product struct {
    ID          int
    Name        string
    Description string
    Price       float64
}

// Plus SQL query
// SELECT id, name, description, price FROM products WHERE id = $1
```

**GORM benefits:**
- Define data once in Go structs
- Type safety at compile time
- Automatic relationship management
- Built-in query builder
- Migrations and schema management

## Getting Started with GORM

### Installation

```bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
```

### Connecting to PostgreSQL

```go
package main

import (
    "log"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

func main() {
    dsn := "host=localhost user=appuser password=secret dbname=myapp port=5432 sslmode=disable"

    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info), // Log SQL queries
    })
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }

    // Get underlying sql.DB for connection pool settings
    sqlDB, err := db.DB()
    if err != nil {
        log.Fatal(err)
    }

    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)
}
```

## GORM Models

### Basic Model Definition

GORM uses struct tags to configure database mapping:

```go
type User struct {
    ID        uint      `gorm:"primaryKey"`
    Username  string    `gorm:"size:50;uniqueIndex;not null"`
    Email     string    `gorm:"size:255;uniqueIndex;not null"`
    Password  string    `gorm:"not null" json:"-"`
    IsActive  bool      `gorm:"default:true"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"` // Soft delete support
}
```

### Embedding gorm.Model

GORM provides a base model with common fields:

```go
// gorm.Model includes:
// - ID uint (primary key)
// - CreatedAt time.Time
// - UpdatedAt time.Time
// - DeletedAt gorm.DeletedAt

type Product struct {
    gorm.Model
    Name        string  `gorm:"size:255;not null"`
    Description string  `gorm:"type:text"`
    Price       float64 `gorm:"not null;check:price > 0"`
    Stock       int     `gorm:"default:0;check:stock >= 0"`
    CategoryID  uint
}
```

### Common GORM Tags

| Tag | Description | Example |
|-----|-------------|---------|
| `primaryKey` | Mark as primary key | `gorm:"primaryKey"` |
| `size` | Column size | `gorm:"size:255"` |
| `type` | Column type | `gorm:"type:text"` |
| `uniqueIndex` | Unique index | `gorm:"uniqueIndex"` |
| `not null` | Not nullable | `gorm:"not null"` |
| `default` | Default value | `gorm:"default:0"` |
| `check` | Check constraint | `gorm:"check:price > 0"` |
| `index` | Create index | `gorm:"index"` |
| `column` | Custom column name | `gorm:"column:user_name"` |

## CRUD Operations

### Create

```go
// Create single record
user := User{Username: "john", Email: "john@example.com", Password: "hashed"}
result := db.Create(&user)

if result.Error != nil {
    log.Error("Failed to create user:", result.Error)
}
fmt.Printf("Created user with ID: %d\n", user.ID)

// Batch create
users := []User{
    {Username: "alice", Email: "alice@example.com"},
    {Username: "bob", Email: "bob@example.com"},
}
db.Create(&users)

// Create with selected fields only
db.Select("Username", "Email").Create(&user)

// Omit specific fields
db.Omit("Password").Create(&user)
```

### Read

```go
// Find by primary key
var user User
db.First(&user, 1) // SELECT * FROM users WHERE id = 1

// Find by primary key with string
db.First(&user, "id = ?", "1")

// Find by condition
db.First(&user, "username = ?", "john")

// Get all records
var users []User
db.Find(&users)

// Where conditions
db.Where("is_active = ?", true).Find(&users)
db.Where("username LIKE ?", "%john%").Find(&users)
db.Where("created_at > ?", time.Now().AddDate(0, -1, 0)).Find(&users)

// Multiple conditions
db.Where("is_active = ?", true).
    Where("created_at > ?", lastWeek).
    Find(&users)

// Or condition
db.Where("username = ?", "john").
    Or("email = ?", "john@example.com").
    First(&user)

// Select specific fields
db.Select("username", "email").Find(&users)

// Order, Limit, Offset
db.Order("created_at desc").Limit(10).Offset(0).Find(&users)
```

### Update

```go
// Update single field
db.Model(&user).Update("username", "johnny")

// Update multiple fields
db.Model(&user).Updates(User{Username: "johnny", Email: "johnny@example.com"})

// Update with map (includes zero values)
db.Model(&user).Updates(map[string]interface{}{
    "username": "johnny",
    "is_active": false,
})

// Update with conditions
db.Model(&User{}).Where("is_active = ?", false).Update("is_active", true)

// Batch update
db.Model(&User{}).Where("created_at < ?", lastYear).Update("is_active", false)
```

### Delete

```go
// Delete by primary key
db.Delete(&user, 1)

// Delete with condition
db.Where("is_active = ?", false).Delete(&User{})

// Soft delete (if DeletedAt field exists)
db.Delete(&user) // Sets deleted_at, doesn't remove row

// Permanently delete (bypass soft delete)
db.Unscoped().Delete(&user)

// Find soft-deleted records
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)
```

## Relationships

### One-to-Many

```go
type Category struct {
    gorm.Model
    Name     string    `gorm:"size:100;not null"`
    Products []Product `gorm:"foreignKey:CategoryID"` // Has Many
}

type Product struct {
    gorm.Model
    Name       string   `gorm:"size:255;not null"`
    Price      float64
    CategoryID uint
    Category   Category `gorm:"foreignKey:CategoryID"` // Belongs To
}
```

```mermaid
erDiagram
    CATEGORY ||--o{ PRODUCT : has
    CATEGORY {
        uint id PK
        string name
    }
    PRODUCT {
        uint id PK
        string name
        float price
        uint category_id FK
    }
```

### Many-to-Many

```go
type Product struct {
    gorm.Model
    Name string
    Tags []Tag `gorm:"many2many:product_tags;"`
}

type Tag struct {
    gorm.Model
    Name     string
    Products []Product `gorm:"many2many:product_tags;"`
}
```

### Working with Relationships

```go
// Create with association
category := Category{Name: "Electronics"}
product := Product{Name: "Laptop", Price: 999.99, Category: category}
db.Create(&product)

// Preload associations
var products []Product
db.Preload("Category").Find(&products)

// Nested preload
db.Preload("Category").Preload("Tags").Find(&products)

// Conditional preload
db.Preload("Tags", "name LIKE ?", "%tech%").Find(&products)

// Append to association
db.Model(&product).Association("Tags").Append(&Tag{Name: "Sale"})

// Remove association
db.Model(&product).Association("Tags").Delete(&tag)

// Replace associations
db.Model(&product).Association("Tags").Replace(&newTags)

// Count associations
count := db.Model(&product).Association("Tags").Count()
```

## GORM Hooks

Hooks allow you to execute code before/after CRUD operations:

```go
type User struct {
    gorm.Model
    Username     string
    Email        string
    PasswordHash string
}

// BeforeCreate hook - hash password before saving
func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.PasswordHash != "" {
        hash, err := bcrypt.GenerateFromPassword([]byte(u.PasswordHash), bcrypt.DefaultCost)
        if err != nil {
            return err
        }
        u.PasswordHash = string(hash)
    }
    return nil
}

// AfterCreate hook - log new user creation
func (u *User) AfterCreate(tx *gorm.DB) error {
    log.Printf("New user created: ID=%d, Username=%s", u.ID, u.Username)
    return nil
}

// BeforeUpdate hook - validate email format
func (u *User) BeforeUpdate(tx *gorm.DB) error {
    if !isValidEmail(u.Email) {
        return errors.New("invalid email format")
    }
    return nil
}

// AfterFind hook - mask sensitive data
func (u *User) AfterFind(tx *gorm.DB) error {
    u.PasswordHash = "" // Clear password hash after loading
    return nil
}
```

```mermaid
flowchart TD
    subgraph Create["Create Operation"]
        BC[BeforeCreate] --> DB1[Database Insert]
        DB1 --> AC[AfterCreate]
    end

    subgraph Read["Read Operation"]
        DB2[Database Select] --> AF[AfterFind]
    end

    subgraph Update["Update Operation"]
        BU[BeforeUpdate] --> DB3[Database Update]
        DB3 --> AU[AfterUpdate]
    end

    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff

    class Create greenClass
    class Read blueClass
    class Update orangeClass
```

## Advanced Queries

### Raw SQL

```go
// Raw query
var users []User
db.Raw("SELECT * FROM users WHERE is_active = ?", true).Scan(&users)

// Exec for non-SELECT statements
db.Exec("UPDATE users SET is_active = ? WHERE last_login < ?", false, lastYear)
```

### Scopes

Reusable query conditions:

```go
// Define scopes
func Active(db *gorm.DB) *gorm.DB {
    return db.Where("is_active = ?", true)
}

func Recent(days int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Where("created_at > ?", time.Now().AddDate(0, 0, -days))
    }
}

func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        offset := (page - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}

// Use scopes
db.Scopes(Active, Recent(7), Paginate(1, 10)).Find(&users)
```

### Joins

```go
type Result struct {
    ProductName  string
    CategoryName string
    Price        float64
}

var results []Result
db.Model(&Product{}).
    Select("products.name as product_name, categories.name as category_name, products.price").
    Joins("LEFT JOIN categories ON products.category_id = categories.id").
    Where("products.price > ?", 100).
    Scan(&results)
```

### Subqueries

```go
// Products with above-average price
avgPrice := db.Model(&Product{}).Select("AVG(price)")
db.Where("price > (?)", avgPrice).Find(&products)

// Users with orders
usersWithOrders := db.Model(&Order{}).Select("user_id")
db.Where("id IN (?)", usersWithOrders).Find(&users)
```

## Transactions

```go
func TransferMoney(db *gorm.DB, fromID, toID uint, amount float64) error {
    return db.Transaction(func(tx *gorm.DB) error {
        // Deduct from sender
        if err := tx.Model(&Account{}).
            Where("id = ? AND balance >= ?", fromID, amount).
            Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil {
            return err
        }

        // Add to receiver
        if err := tx.Model(&Account{}).
            Where("id = ?", toID).
            Update("balance", gorm.Expr("balance + ?", amount)).Error; err != nil {
            return err
        }

        // Create transaction record
        record := Transaction{FromID: fromID, ToID: toID, Amount: amount}
        if err := tx.Create(&record).Error; err != nil {
            return err
        }

        return nil // Commit
    })
}
```

## Auto-Migration

```go
// Auto-migrate creates/updates tables
db.AutoMigrate(&User{}, &Product{}, &Category{}, &Order{})

// This will:
// - Create tables if they don't exist
// - Add missing columns
// - Add missing indexes
// - NOT delete columns or change types (safe by default)
```

## Error Handling

```go
func GetUser(db *gorm.DB, id uint) (*User, error) {
    var user User
    result := db.First(&user, id)

    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            return nil, ErrUserNotFound
        }
        return nil, result.Error
    }

    return &user, nil
}

// Check rows affected
result := db.Model(&User{}).Where("id = ?", id).Update("is_active", false)
if result.RowsAffected == 0 {
    return ErrUserNotFound
}
```

## Best Practices

### Repository Pattern

```go
type UserRepository interface {
    Create(user *User) error
    FindByID(id uint) (*User, error)
    FindByEmail(email string) (*User, error)
    Update(user *User) error
    Delete(id uint) error
    List(page, pageSize int) ([]User, int64, error)
}

type userRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(id uint) (*User, error) {
    var user User
    if err := r.db.First(&user, id).Error; err != nil {
        return nil, err
    }
    return &user, nil
}

func (r *userRepository) List(page, pageSize int) ([]User, int64, error) {
    var users []User
    var total int64

    r.db.Model(&User{}).Count(&total)
    err := r.db.Scopes(Paginate(page, pageSize)).Find(&users).Error

    return users, total, err
}
```

## Summary

| Feature | Key Points |
|---------|------------|
| **Models** | Define schema with struct tags |
| **CRUD** | Create, First, Find, Update, Delete |
| **Relationships** | BelongsTo, HasMany, ManyToMany with Preload |
| **Hooks** | BeforeCreate, AfterCreate, BeforeUpdate, etc. |
| **Scopes** | Reusable query conditions |
| **Transactions** | db.Transaction for atomic operations |

---

*Next post: [Database Migrations in Go](https://vanducng.dev/2024/03/22/Database-Migrations-in-Go/) - Managing schema evolution safely with migration tools.*
