GORM #

GORM is the most popular ORM (Object-Relational Mapper) in the Go ecosystem — used by millions of projects with over 35,000 stars on GitHub. An ORM lets you work with databases using ordinary Go structs instead of writing raw SQL, which speeds up development and reduces boilerplate. GORM supports MySQL, PostgreSQL, SQLite, and SQL Server with a uniform API — switching databases only requires changing the driver and DSN.

ORM vs Raw SQL: GORM is ideal for standard CRUD, relationships, and fast prototyping. For complex analytical queries, reports with many joins, or performance-critical code, raw SQL (direct database/sql) is still more appropriate. A hybrid approach — GORM for common operations, raw SQL for specialized queries — is the most pragmatic.

Installation #

# GORM core
go get gorm.io/gorm

# Driver — choose based on your database
go get gorm.io/driver/mysql      # MySQL
go get gorm.io/driver/postgres   # PostgreSQL
go get gorm.io/driver/sqlite     # SQLite
go get gorm.io/driver/sqlserver  # SQL Server

Connecting to a Database #

import (
    "gorm.io/driver/mysql"
    "gorm.io/driver/postgres"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

// MySQL
dsn := "root:password@tcp(localhost:3306)/onlinestore?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

// PostgreSQL
dsn = "host=localhost user=postgres password=password dbname=onlinestore port=5432 sslmode=disable"
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

// SQLite — ideal for development and testing
db, err = gorm.Open(sqlite.Open("onlinestore.db"), &gorm.Config{})

// Logger configuration
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: logger.Default.LogMode(logger.Info), // show all SQL
})

// Access the *sql.DB behind GORM to set up the pool
sqlDB, err := db.DB()
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(25)
sqlDB.SetConnMaxLifetime(5 * time.Minute)

Defining Models #

GORM uses Go structs as models. Built-in conventions — overridable with tags:

import "gorm.io/gorm"

// gorm.Model includes ID, CreatedAt, UpdatedAt, DeletedAt (soft delete)
type Product struct {
    gorm.Model                    // embed: ID uint, CreatedAt, UpdatedAt, DeletedAt

    Name        string           `gorm:"size:200;not null;uniqueIndex"`
    Description string           `gorm:"type:text"`
    Price       float64          `gorm:"not null;default:0"`
    Stock       int              `gorm:"not null;default:0"`
    CategoryID  uint             `gorm:"not null;index"`
    IsActive    bool             `gorm:"default:true"`

    // Associations
    Category    Category         `gorm:"foreignKey:CategoryID"`
    Tags        []Tag            `gorm:"many2many:product_tags;"`
    Images      []ProductImage   `gorm:"foreignKey:ProductID"`
}

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

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

type ProductImage struct {
    gorm.Model
    ProductID uint   `gorm:"not null;index"`
    URL       string `gorm:"size:500;not null"`
    IsPrimary bool   `gorm:"default:false"`
}

// A model without gorm.Model — full control over the fields
type Order struct {
    ID         uint      `gorm:"primaryKey;autoIncrement"`
    CreatedAt  time.Time
    UpdatedAt  time.Time
    CustomerID uint      `gorm:"not null;index"`
    Total      float64   `gorm:"not null;default:0"`
    Status     string    `gorm:"size:50;default:'pending'"`
    Note       string    `gorm:"type:text"`

    Customer   Customer  `gorm:"foreignKey:CustomerID"`
    Items      []OrderItem
}

type OrderItem struct {
    ID        uint    `gorm:"primaryKey;autoIncrement"`
    OrderID   uint    `gorm:"not null;index"`
    ProductID uint    `gorm:"not null"`
    Qty       int     `gorm:"not null;default:1"`
    Price     float64 `gorm:"not null"`
    Product   Product `gorm:"foreignKey:ProductID"`
}

type Customer struct {
    gorm.Model
    Name   string  `gorm:"size:100;not null"`
    Email  string  `gorm:"size:100;not null;uniqueIndex"`
    Phone  string  `gorm:"size:20"`
    Orders []Order `gorm:"foreignKey:CustomerID"`
}

GORM Conventions #

Product struct name → products table (plural, snake_case)
OrderItem struct name → order_items table
ID uint field → primary key
CreatedAt time.Time field → auto-set on create
UpdatedAt time.Time field → auto-set on update
DeletedAt gorm.DeletedAt field → soft delete if present

Override conventions with tags:
  `gorm:"table:custom_name"` → custom table name
  `gorm:"column:custom_col"` → custom column name
  `gorm:"primaryKey"` → mark as primary key
  `gorm:"autoIncrement"` → auto increment
  `gorm:"not null"` → NOT NULL constraint
  `gorm:"uniqueIndex"` → unique index
  `gorm:"index"` → regular index
  `gorm:"default:value"` → default value
  `gorm:"size:200"` → VARCHAR(200)
  `gorm:"type:text"` → specific column type
  `gorm:"-"` → ignore this field

Auto Migration #

GORM can create or update table schemas automatically:

// AutoMigrate creates tables, columns, and indexes that don't exist yet
// It does NOT drop existing columns (safe for production)
err := db.AutoMigrate(
    &Category{},
    &Tag{},
    &Product{},
    &ProductImage{},
    &Customer{},
    &Order{},
    &OrderItem{},
)
if err != nil {
    log.Fatal("AutoMigrate:", err)
}

How GORM Auto-Migration Works #

GORM’s automatic migration mechanism is minimally destructive. It only adds new tables, columns, or indexes, never deleting existing data or columns:

flowchart TD
    Start["db.AutoMigrate(&Model{})"] --> ReadStruct["GORM Scans Struct Structure & Tags"]
    ReadStruct --> CheckTable{"Table Exists in Database?"}
    
    CheckTable -->|"No"| CreateTable["CREATE TABLE with all columns"]
    CheckTable -->|"Yes"| CompareSchema["Compare Struct Schema vs DB Table"]

    CompareSchema --> CheckDiff{"New Columns/Indexes?"}
    CheckDiff -->|"Yes"| AlterTable["ALTER TABLE ... ADD COLUMN / CREATE INDEX"]
    CheckDiff -->|"No"| Safe["Schema matches (No-Op)"]
    
    CreateTable --> Done["Migration Successful"]
    AlterTable --> Done
    Safe --> Done

Create — Creating Records #

// Create a single record
product := Product{
    Name:       "Pro Laptop 14",
    Price:      15_000_000,
    Stock:      10,
    CategoryID: 1,
}
result := db.Create(&product)
if result.Error != nil {
    log.Fatal("Failed to create:", result.Error)
}
fmt.Println("New ID:", product.ID)  // GORM auto-sets the ID after Create

// Create with selected fields only
db.Select("Name", "Price").Create(&product)

// Create many records at once
products := []Product{
    {Name: "Wireless Mouse", Price: 350_000, Stock: 50, CategoryID: 1},
    {Name: "Mech Keyboard", Price: 1_500_000, Stock: 25, CategoryID: 1},
}
db.Create(&products)
// Each product's ID is filled after Create

// Upsert — create or update on conflict
db.Save(&product)  // inserts if ID=0, updates if ID > 0

// Create or update based on a specific field
db.Where(Product{Name: "Pro Laptop 14"}).
    Attrs(Product{Stock: 10}).    // only set if it's a new record
    FirstOrCreate(&product)

Read — Reading Data #

// Get a single record by primary key
var p Product
db.First(&p, 1)                           // WHERE id = 1 ORDER BY id
db.First(&p, "id = ?", 1)                 // equivalent
db.Take(&p, 1)                            // without ORDER BY — faster

// With error handling
result := db.First(&p, 999)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
    fmt.Println("Product not found")
}

// Find — many records
var products []Product
db.Find(&products)                        // all products
db.Find(&products, "category_id = ?", 1)  // with a condition

// Where — various ways
db.Where("name = ?", "Pro Laptop 14").First(&p)
db.Where("price BETWEEN ? AND ?", 100_000, 5_000_000).Find(&products)
db.Where("name LIKE ?", "%laptop%").Find(&products)
db.Where("stock > ? AND is_active = ?", 0, true).Find(&products)

// Where with a struct — only non-zero fields are used
db.Where(&Product{CategoryID: 1, IsActive: true}).Find(&products)

// Where with a map — more explicit, including zero values
db.Where(map[string]interface{}{
    "category_id": 1,
    "is_active":   true,
    "stock":       0,  // can use 0 as a value
}).Find(&products)

// Select specific fields
db.Select("id", "name", "price").Find(&products)

// Order, Limit, Offset
db.Order("price DESC").Limit(10).Offset(20).Find(&products)

// Count
var count int64
db.Model(&Product{}).Where("category_id = ?", 1).Count(&count)

// Pluck — get one column as a slice
var names []string
db.Model(&Product{}).Pluck("name", &names)

// Scan into a custom struct (for JOIN/aggregate results)
type ProductSummary struct {
    CategoryName string
    Count        int
    AvgPrice     float64
}
var summary []ProductSummary
db.Model(&Product{}).
    Select("categories.name AS category_name, COUNT(*) AS count, AVG(price) AS avg_price").
    Joins("JOIN categories ON categories.id = products.category_id").
    Group("categories.name").
    Scan(&summary)

Update — Updating Data #

// Update all changed fields (Save)
p.Price = 14_500_000
p.Stock = 8
db.Save(&p)  // UPDATE all non-zero columns

// Update specific fields only
db.Model(&p).Update("price", 14_500_000)
db.Model(&p).Updates(Product{Price: 14_500_000, Stock: 8})
db.Model(&p).Updates(map[string]interface{}{
    "price": 14_500_000,
    "stock": 0,  // can update to zero with a map
})

// Update without fetching first (more efficient)
db.Model(&Product{}).Where("category_id = ?", 1).
    Update("is_active", false)

// Update with a SQL expression
db.Model(&Product{}).Where("id = ?", 1).
    UpdateColumn("stock", gorm.Expr("stock - ?", 5))

Delete — Deleting Data #

// Soft delete — sets DeletedAt, the data isn't actually removed
db.Delete(&p)             // SET deleted_at = NOW()
db.Delete(&Product{}, 1)  // with an ID

// Hard delete — permanent removal
db.Unscoped().Delete(&p)

// Delete with a condition
db.Where("stock = 0").Delete(&Product{})

// Query soft-deleted data
var products []Product
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&products)

Associations #

Preloading — Eager Loading #

// Load one association
var product Product
db.Preload("Category").First(&product, 1)
// SELECT * FROM products WHERE id=1;
// SELECT * FROM categories WHERE id=product.CategoryID;

// Load many associations at once
db.Preload("Category").Preload("Tags").Preload("Images").First(&product, 1)

// Nested preload
db.Preload("Orders.Items.Product").First(&customer, 1)

// Preload with a condition
db.Preload("Images", "is_primary = ?", true).Find(&products)

// Preload everything (careful — N+1 queries!)
db.Preload(clause.Associations).First(&product, 1)

Creating with Associations #

// Create a parent and its children at once
product := Product{
    Name:  "Gaming Laptop",
    Price: 20_000_000,
    Tags: []Tag{
        {Name: "gaming"},
        {Name: "laptop"},
    },
    Images: []ProductImage{
        {URL: "https://img.example.com/laptop.jpg", IsPrimary: true},
    },
}
db.Create(&product)  // GORM auto-creates all associations

// Add an association to an existing record
var tags []Tag
db.Find(&tags, []uint{1, 2, 3})
db.Model(&product).Association("Tags").Append(&tags)

// Remove an association (many2many — only deletes the join table row)
db.Model(&product).Association("Tags").Delete(&tags)

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

Hooks — Lifecycle Callbacks #

Hooks (or callbacks) are functions automatically called before or after certain database operations. GORM groups the callback execution order by operation type:

Operation CategoryHook Execution Order (Lifecycle)Example Use Cases
Create (INSERT)BeforeSaveBeforeCreate[Write DB]AfterCreateAfterSaveInput validation, slug generation, data encryption
Update (UPDATE)BeforeSaveBeforeUpdate[Update DB]AfterUpdateAfterSaveCache cleanup, manual timestamp updates
Delete (DELETE)BeforeDelete[Delete DB]AfterDeleteCascading deletes of related records, audit trail logs
Query (SELECT)[Read DB]AfterFindDecrypting sensitive data, parsing calculated values
type Product struct {
    gorm.Model
    Name  string
    Price float64
    Slug  string
}

// BeforeCreate — runs before INSERT
func (p *Product) BeforeCreate(tx *gorm.DB) error {
    // Validation
    if p.Price < 0 {
        return errors.New("price cannot be negative")
    }
    // Auto-generate a slug from the name
    p.Slug = slug.Make(p.Name)
    return nil
}

// AfterCreate — runs after a successful INSERT
func (p *Product) AfterCreate(tx *gorm.DB) error {
    // Send a notification, update a cache, etc.
    log.Printf("New product created: %s (ID: %d)", p.Name, p.ID)
    return nil
}

// BeforeUpdate — validation before UPDATE
func (p *Product) BeforeUpdate(tx *gorm.DB) error {
    if p.Price < 0 {
        return errors.New("price cannot be negative")
    }
    return nil
}

// BeforeDelete — runs before DELETE
func (p *Product) BeforeDelete(tx *gorm.DB) error {
    // Check whether any active orders use this product
    var count int64
    tx.Model(&OrderItem{}).
        Joins("JOIN orders ON orders.id = order_items.order_id").
        Where("order_items.product_id = ? AND orders.status != 'completed'", p.ID).
        Count(&count)
    if count > 0 {
        return fmt.Errorf("cannot delete — there are %d active orders", count)
    }
    return nil
}

Transactions #

// Automatic transaction with a closure
err := db.Transaction(func(tx *gorm.DB) error {
    // Use tx (not db) inside the transaction
    if err := tx.Create(&order).Error; err != nil {
        return err  // auto rollback if an error is returned
    }

    for _, item := range order.Items {
        // Reduce the stock
        result := tx.Model(&Product{}).
            Where("id = ? AND stock >= ?", item.ProductID, item.Qty).
            UpdateColumn("stock", gorm.Expr("stock - ?", item.Qty))

        if result.Error != nil {
            return result.Error
        }
        if result.RowsAffected == 0 {
            return fmt.Errorf("insufficient stock for product %d", item.ProductID)
        }
    }

    return nil  // commit if there are no errors
})

// Manual transaction
tx := db.Begin()
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

if err := tx.Create(&order).Error; err != nil {
    tx.Rollback()
    return err
}
tx.Commit()

Scopes — Reusable Query Conditions #

// Scope definition — a function that modifies a query
func Active(db *gorm.DB) *gorm.DB {
    return db.Where("is_active = ?", true)
}

func InStock(db *gorm.DB) *gorm.DB {
    return db.Where("stock > 0")
}

func ByCategory(categoryID uint) func(*gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Where("category_id = ?", categoryID)
    }
}

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

func PriceRange(min, max float64) func(*gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Where("price BETWEEN ? AND ?", min, max)
    }
}

// Usage — chainable
var products []Product
db.Scopes(Active, InStock, ByCategory(1), Paginate(1, 10)).
    Order("price ASC").
    Find(&products)

// With a price range
db.Scopes(Active, PriceRange(100_000, 5_000_000)).
    Find(&products)

Raw SQL with GORM #

When you need complex queries that can’t be expressed with the GORM API:

// Raw query into a struct
type RevenueReport struct {
    Month    string
    Category string
    Revenue  float64
    Orders   int
}

var report []RevenueReport
db.Raw(`
    SELECT
        DATE_FORMAT(o.created_at, '%Y-%m') AS month,
        c.name AS category,
        SUM(oi.price * oi.qty) AS revenue,
        COUNT(DISTINCT o.id) AS orders
    FROM orders o
    JOIN order_items oi ON oi.order_id = o.id
    JOIN products p ON p.id = oi.product_id
    JOIN categories c ON c.id = p.category_id
    WHERE o.status = 'completed'
      AND o.created_at >= ?
    GROUP BY month, c.name
    ORDER BY month DESC, revenue DESC
`, time.Now().AddDate(0, -6, 0)).Scan(&report)

// Exec for DDL / DML that doesn't return rows
db.Exec("UPDATE products SET stock = 0 WHERE deleted_at IS NOT NULL")
db.Exec("CREATE INDEX IF NOT EXISTS idx_price ON products(price)")

Complete Example Program #

package main

import (
    "errors"
    "fmt"
    "log"
    "time"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/clause"
    "gorm.io/gorm/logger"
)

// ── Models ────────────────────────────────────────────────────

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

type Product struct {
    gorm.Model
    Name       string   `gorm:"size:200;not null"`
    Price      float64  `gorm:"not null;default:0"`
    Stock      int      `gorm:"not null;default:0"`
    CategoryID uint     `gorm:"not null;index"`
    IsActive   bool     `gorm:"default:true"`
    Category   Category `gorm:"foreignKey:CategoryID"`
}

func (p *Product) BeforeCreate(tx *gorm.DB) error {
    if p.Price < 0 {
        return errors.New("price cannot be negative")
    }
    return nil
}

type Customer struct {
    gorm.Model
    Name   string  `gorm:"size:100;not null"`
    Email  string  `gorm:"size:100;not null;uniqueIndex"`
    Orders []Order `gorm:"foreignKey:CustomerID"`
}

type Order struct {
    gorm.Model
    CustomerID uint        `gorm:"not null;index"`
    Total      float64     `gorm:"not null;default:0"`
    Status     string      `gorm:"size:50;default:'pending'"`
    Customer   Customer    `gorm:"foreignKey:CustomerID"`
    Items      []OrderItem `gorm:"foreignKey:OrderID"`
}

type OrderItem struct {
    gorm.Model
    OrderID   uint    `gorm:"not null;index"`
    ProductID uint    `gorm:"not null"`
    Qty       int     `gorm:"not null;default:1"`
    Price     float64 `gorm:"not null"`
    Product   Product `gorm:"foreignKey:ProductID"`
}

// ── Scopes ────────────────────────────────────────────────────

func Active(db *gorm.DB) *gorm.DB  { return db.Where("is_active = ?", true) }
func InStock(db *gorm.DB) *gorm.DB { return db.Where("stock > 0") }
func Paginate(page, perPage int) func(*gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Offset((page - 1) * perPage).Limit(perPage)
    }
}

// ── Service ───────────────────────────────────────────────────

func placeOrder(db *gorm.DB, customerID uint, items []OrderItem) (*Order, error) {
    var order Order

    err := db.Transaction(func(tx *gorm.DB) error {
        // Calculate the total and validate stock
        total := 0.0
        for i := range items {
            var p Product
            if err := tx.First(&p, items[i].ProductID).Error; err != nil {
                return fmt.Errorf("product %d not found", items[i].ProductID)
            }
            if p.Stock < items[i].Qty {
                return fmt.Errorf("insufficient stock for %s (available: %d)", p.Name, p.Stock)
            }
            items[i].Price = p.Price
            total += p.Price * float64(items[i].Qty)

            // Reduce the stock
            if err := tx.Model(&p).UpdateColumn("stock",
                gorm.Expr("stock - ?", items[i].Qty)).Error; err != nil {
                return err
            }
        }

        // Create the order
        order = Order{
            CustomerID: customerID,
            Total:      total,
            Status:     "pending",
            Items:      items,
        }
        return tx.Create(&order).Error
    })

    return &order, err
}

// ── Main ──────────────────────────────────────────────────────

func main() {
    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Warn),
    })
    if err != nil {
        log.Fatal(err)
    }

    // Migrate
    db.AutoMigrate(&Category{}, &Product{}, &Customer{}, &Order{}, &OrderItem{})

    // Seed categories
    categories := []Category{
        {Name: "Electronics"},
        {Name: "Fashion"},
        {Name: "Books"},
    }
    db.Create(&categories)

    // Seed products
    products := []Product{
        {Name: "Pro Laptop 14", Price: 15_000_000, Stock: 10, CategoryID: categories[0].ID},
        {Name: "Wireless Mouse", Price: 350_000, Stock: 50, CategoryID: categories[0].ID},
        {Name: "Mech Keyboard", Price: 1_500_000, Stock: 25, CategoryID: categories[0].ID},
        {Name: "Plain T-Shirt", Price: 85_000, Stock: 100, CategoryID: categories[1].ID},
        {Name: "Go Programming Book", Price: 180_000, Stock: 30, CategoryID: categories[2].ID},
    }
    db.Create(&products)

    // Seed a customer
    customer := Customer{Name: "Budi Santoso", Email: "[email protected]"}
    db.Create(&customer)

    fmt.Println("=== Data Seeded ===")

    // Query with scopes and preload
    fmt.Println("\n=== Active In-Stock Products (Electronics) ===")
    var electronics []Product
    db.Scopes(Active, InStock).
        Where("category_id = ?", categories[0].ID).
        Preload("Category").
        Order("price ASC").
        Find(&electronics)
    for _, p := range electronics {
        fmt.Printf("  %-20s %-12s Rp%.0f (stock: %d)\n",
            p.Name, p.Category.Name, p.Price, p.Stock)
    }

    // Place an order
    fmt.Println("\n=== Place Order ===")
    order, err := placeOrder(db, customer.ID, []OrderItem{
        {ProductID: products[0].ID, Qty: 1},
        {ProductID: products[1].ID, Qty: 2},
    })
    if err != nil {
        fmt.Println("  Order failed:", err)
    } else {
        fmt.Printf("  Order #%d successful! Total: Rp%.0f\n", order.ID, order.Total)
    }

    // Load the order with all associations
    fmt.Println("\n=== Order Details ===")
    var loadedOrder Order
    db.Preload(clause.Associations).
        Preload("Items.Product").
        First(&loadedOrder, order.ID)

    fmt.Printf("  Order #%d — %s (Total: Rp%.0f)\n",
        loadedOrder.ID, loadedOrder.Customer.Name, loadedOrder.Total)
    for _, item := range loadedOrder.Items {
        fmt.Printf("    - %-20s x%d @ Rp%.0f = Rp%.0f\n",
            item.Product.Name, item.Qty,
            item.Price, item.Price*float64(item.Qty))
    }

    // Check the stock after the order
    fmt.Println("\n=== Stock After Order ===")
    db.Where("id IN ?", []uint{products[0].ID, products[1].ID}).Find(&products[:2])
    for _, p := range products[:2] {
        db.First(&p, p.ID)
        fmt.Printf("  %-20s stock: %d\n", p.Name, p.Stock)
    }

    // Soft delete
    fmt.Println("\n=== Soft Delete ===")
    db.Delete(&products[4])  // delete "Go Programming Book"
    var count int64
    db.Model(&Product{}).Count(&count)
    fmt.Printf("  Active products: %d (1 has been soft-deleted)\n", count)

    // Query including soft-deleted records
    db.Unscoped().Model(&Product{}).Count(&count)
    fmt.Printf("  Total including deleted: %d\n", count)

    // Statistics with raw SQL
    fmt.Println("\n=== Statistics per Category ===")
    type CatStats struct {
        CategoryName string
        ProductCount int64
        TotalStock   int64
        AvgPrice     float64
    }
    var stats []CatStats
    db.Model(&Product{}).
        Select("categories.name AS category_name, COUNT(*) AS product_count, SUM(products.stock) AS total_stock, AVG(products.price) AS avg_price").
        Joins("JOIN categories ON categories.id = products.category_id").
        Group("categories.name").
        Scan(&stats)

    for _, s := range stats {
        fmt.Printf("  %-12s: %d products, %d stock, avg Rp%.0f\n",
            s.CategoryName, s.ProductCount, s.TotalStock, s.AvgPrice)
    }
}

Summary #

  • GORM is ideal for standard CRUD and relationships — use raw SQL for complex queries or performance-critical code.
  • gorm.Model automatically includes ID, CreatedAt, UpdatedAt, DeletedAt (soft delete).
  • AutoMigrate is safe for production — it only adds, never removes existing columns.
  • db.Create(&p) auto-sets ID, CreatedAt, UpdatedAt after success.
  • db.First returns gorm.ErrRecordNotFound when nothing is found — check with errors.Is.
  • db.Save updates if ID > 0, inserts if ID = 0; use db.Updates to update specific fields only.
  • Preload for eager loading associations — avoid N+1 queries; use clause.Associations for everything.
  • Hooks (BeforeCreate, AfterUpdate, etc.) for lifecycle logic — return an error to cancel the operation.
  • Scopes for reusable query conditions — db.Scopes(Active, InStock, Paginate(1, 10)).
  • db.Transaction(func(tx *gorm.DB) error {...}) — auto commit/rollback based on the return value.
  • Soft delete is automatic if the model has a DeletedAt gorm.DeletedAt field — use db.Unscoped() to access deleted data.

← Previous: PostgreSQL   Next: MongoDB →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact