unisbadri.com » Python Java Golang Typescript Kotlin Ruby Rust Dart PHP

GORM #

GORM adalah ORM (Object-Relational Mapper) paling populer di ekosistem Go — digunakan oleh jutaan project dan memiliki lebih dari 35.000 bintang di GitHub. ORM memungkinkan kamu bekerja dengan database menggunakan struct Go biasa alih-alih menulis SQL mentah, yang mempercepat pengembangan dan mengurangi boilerplate. GORM mendukung MySQL, PostgreSQL, SQLite, dan SQL Server dengan API yang seragam — berpindah database hanya butuh mengganti driver dan DSN.

ORM vs Raw SQL: GORM ideal untuk CRUD standar, relasi, dan prototyping cepat. Untuk query analitik kompleks, laporan dengan banyak join, atau performa kritis, raw SQL (database/sql langsung) masih lebih tepat. Pendekatan hybrid — GORM untuk operasi umum, raw SQL untuk query khusus — adalah yang paling pragmatis.

Instalasi #

# GORM core
go get gorm.io/gorm

# Driver — pilih sesuai database kamu
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

Koneksi ke 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)/tokoonline?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

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

// SQLite — ideal untuk development dan testing
db, err = gorm.Open(sqlite.Open("tokoonline.db"), &gorm.Config{})

// Konfigurasi logger
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: logger.Default.LogMode(logger.Info), // tampilkan semua SQL
})

// Akses *sql.DB di balik GORM untuk set pool
sqlDB, err := db.DB()
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(25)
sqlDB.SetConnMaxLifetime(5 * time.Minute)

Mendefinisikan Model #

GORM menggunakan struct Go sebagai model. Konvensi bawaan — bisa di-override dengan tag:

import "gorm.io/gorm"

// gorm.Model menyertakan 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"`
}

// Model tanpa gorm.Model — kontrol penuh atas 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"`
}

Konvensi GORM #

Nama struct Product → tabel products (jamak, snake_case)
Nama struct OrderItem → tabel order_items
Field ID uint → primary key
Field CreatedAt time.Time → auto-set saat create
Field UpdatedAt time.Time → auto-set saat update
Field DeletedAt gorm.DeletedAt → soft delete jika ada

Override konvensi dengan tag:
  `gorm:"table:custom_name"` → nama tabel kustom
  `gorm:"column:custom_col"` → nama kolom kustom
  `gorm:"primaryKey"` → tandai sebagai primary key
  `gorm:"autoIncrement"` → auto increment
  `gorm:"not null"` → NOT NULL constraint
  `gorm:"uniqueIndex"` → unique index
  `gorm:"index"` → regular index
  `gorm:"default:nilai"` → default value
  `gorm:"size:200"` → VARCHAR(200)
  `gorm:"type:text"` → tipe kolom spesifik
  `gorm:"-"` → abaikan field ini

Auto Migration #

GORM bisa membuat atau memperbarui schema tabel secara otomatis:

// AutoMigrate membuat tabel, kolom, dan index yang belum ada
// TIDAK menghapus kolom yang sudah ada (aman untuk production)
err := db.AutoMigrate(
    &Category{},
    &Tag{},
    &Product{},
    &ProductImage{},
    &Customer{},
    &Order{},
    &OrderItem{},
)
if err != nil {
    log.Fatal("AutoMigrate:", err)
}

Create — Membuat Record #

// Create satu record
product := Product{
    Name:       "Laptop Pro 14",
    Price:      15_000_000,
    Stock:      10,
    CategoryID: 1,
}
result := db.Create(&product)
if result.Error != nil {
    log.Fatal("Gagal create:", result.Error)
}
fmt.Println("ID baru:", product.ID)  // GORM auto-set ID setelah Create

// Create dengan selected fields saja
db.Select("Name", "Price").Create(&product)

// Create banyak record sekaligus
products := []Product{
    {Name: "Mouse Wireless", Price: 350_000, Stock: 50, CategoryID: 1},
    {Name: "Keyboard Mech", Price: 1_500_000, Stock: 25, CategoryID: 1},
}
db.Create(&products)
// ID masing-masing produk terisi setelah Create

// Upsert — create or update jika conflict
db.Save(&product)  // insert jika ID=0, update jika ID > 0

// Create or update berdasarkan field tertentu
db.Where(Product{Name: "Laptop Pro 14"}).
    Attrs(Product{Stock: 10}).    // hanya set jika record baru
    FirstOrCreate(&product)

Read — Membaca Data #

// Ambil satu record berdasarkan primary key
var p Product
db.First(&p, 1)                           // WHERE id = 1 ORDER BY id
db.First(&p, "id = ?", 1)                 // ekuivalen
db.Take(&p, 1)                            // tanpa ORDER BY — lebih cepat

// Dengan error handling
result := db.First(&p, 999)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
    fmt.Println("Produk tidak ditemukan")
}

// Find — banyak record
var products []Product
db.Find(&products)                        // semua produk
db.Find(&products, "category_id = ?", 1) // dengan kondisi

// Where — berbagai cara
db.Where("name = ?", "Laptop Pro 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 dengan struct — hanya non-zero fields yang dipakai
db.Where(&Product{CategoryID: 1, IsActive: true}).Find(&products)

// Where dengan map — lebih eksplisit, termasuk zero values
db.Where(map[string]interface{}{
    "category_id": 1,
    "is_active":   true,
    "stock":       0,  // bisa pakai 0 sebagai nilai
}).Find(&products)

// Select field tertentu
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 — ambil satu kolom sebagai slice
var names []string
db.Model(&Product{}).Pluck("name", &names)

// Scan ke struct kustom (untuk hasil JOIN/aggregate)
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 — Memperbarui Data #

// Update semua field yang berubah (Save)
p.Price = 14_500_000
p.Stock = 8
db.Save(&p)  // UPDATE semua kolom non-zero

// Update field tertentu saja
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,  // bisa update ke zero dengan map
})

// Update tanpa fetch dulu (lebih efisien)
db.Model(&Product{}).Where("category_id = ?", 1).
    Update("is_active", false)

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

Delete — Menghapus Data #

// Soft delete — set DeletedAt, data tidak benar-benar dihapus
db.Delete(&p)             // SET deleted_at = NOW()
db.Delete(&Product{}, 1)  // dengan ID

// Hard delete — hapus permanen
db.Unscoped().Delete(&p)

// Delete dengan kondisi
db.Where("stock = 0").Delete(&Product{})

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

Associations #

Preloading — Eager Loading #

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

// Load banyak association sekaligus
db.Preload("Category").Preload("Tags").Preload("Images").First(&product, 1)

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

// Preload dengan kondisi
db.Preload("Images", "is_primary = ?", true).Find(&products)

// Preload semua (hati-hati N+1 query!)
db.Preload(clause.Associations).First(&product, 1)

Create dengan Association #

// Create parent + child sekaligus
product := Product{
    Name:  "Laptop Gaming",
    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-create semua association

// Tambah association ke record yang sudah ada
var tags []Tag
db.Find(&tags, []uint{1, 2, 3})
db.Model(&product).Association("Tags").Append(&tags)

// Hapus association (many2many — hanya hapus join table)
db.Model(&product).Association("Tags").Delete(&tags)

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

Hooks — Lifecycle Callbacks #

type Product struct {
    gorm.Model
    Name  string
    Price float64
    Slug  string
}

// BeforeCreate — jalankan sebelum INSERT
func (p *Product) BeforeCreate(tx *gorm.DB) error {
    // Validasi
    if p.Price < 0 {
        return errors.New("harga tidak boleh negatif")
    }
    // Auto-generate slug dari name
    p.Slug = slug.Make(p.Name)
    return nil
}

// AfterCreate — jalankan setelah INSERT berhasil
func (p *Product) AfterCreate(tx *gorm.DB) error {
    // Kirim notifikasi, update cache, dll
    log.Printf("Produk baru dibuat: %s (ID: %d)", p.Name, p.ID)
    return nil
}

// BeforeUpdate — validasi sebelum UPDATE
func (p *Product) BeforeUpdate(tx *gorm.DB) error {
    if p.Price < 0 {
        return errors.New("harga tidak boleh negatif")
    }
    return nil
}

// BeforeDelete — jalankan sebelum DELETE
func (p *Product) BeforeDelete(tx *gorm.DB) error {
    // Cek apakah ada order aktif yang menggunakan produk ini
    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("tidak bisa hapus — ada %d order aktif", count)
    }
    return nil
}

Transaksi #

// Transaksi otomatis dengan closure
err := db.Transaction(func(tx *gorm.DB) error {
    // Gunakan tx (bukan db) di dalam transaksi
    if err := tx.Create(&order).Error; err != nil {
        return err  // auto rollback jika return error
    }

    for _, item := range order.Items {
        // Kurangi stok
        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("stok produk %d tidak mencukupi", item.ProductID)
        }
    }

    return nil  // commit jika tidak ada error
})

// Transaksi manual
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 #

// Definisi scope — fungsi yang memodifikasi 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)
    }
}

// Penggunaan — bisa di-chain
var products []Product
db.Scopes(Active, InStock, ByCategory(1), Paginate(1, 10)).
    Order("price ASC").
    Find(&products)

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

Raw SQL dengan GORM #

Ketika butuh query kompleks yang tidak bisa diekspresikan dengan GORM API:

// Raw query ke 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 untuk DDL / DML yang tidak 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)")

Contoh Program Lengkap #

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("harga tidak boleh negatif")
    }
    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 {
        // Hitung total dan validasi stok
        total := 0.0
        for i := range items {
            var p Product
            if err := tx.First(&p, items[i].ProductID).Error; err != nil {
                return fmt.Errorf("produk %d tidak ditemukan", items[i].ProductID)
            }
            if p.Stock < items[i].Qty {
                return fmt.Errorf("stok %s tidak mencukupi (tersedia: %d)", p.Name, p.Stock)
            }
            items[i].Price = p.Price
            total += p.Price * float64(items[i].Qty)

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

        // Buat 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: "Elektronik"},
        {Name: "Fashion"},
        {Name: "Buku"},
    }
    db.Create(&categories)

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

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

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

    // Query dengan scope dan preload
    fmt.Println("\n=== Produk Aktif Berstock (Elektronik) ===")
    var elekt []Product
    db.Scopes(Active, InStock).
        Where("category_id = ?", categories[0].ID).
        Preload("Category").
        Order("price ASC").
        Find(&elekt)
    for _, p := range elekt {
        fmt.Printf("  %-20s %-12s Rp%.0f (stok: %d)\n",
            p.Name, p.Category.Name, p.Price, p.Stock)
    }

    // Place 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 gagal:", err)
    } else {
        fmt.Printf("  Order #%d berhasil! Total: Rp%.0f\n", order.ID, order.Total)
    }

    // Load order dengan semua association
    fmt.Println("\n=== Detail Order ===")
    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))
    }

    // Cek stok setelah order
    fmt.Println("\n=== Stok Setelah 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 stok: %d\n", p.Name, p.Stock)
    }

    // Soft delete
    fmt.Println("\n=== Soft Delete ===")
    db.Delete(&products[4])  // hapus "Buku Go Programming"
    var count int64
    db.Model(&Product{}).Count(&count)
    fmt.Printf("  Produk aktif: %d (1 sudah di-soft delete)\n", count)

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

    // Statistik dengan raw SQL
    fmt.Println("\n=== Statistik per Kategori ===")
    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 produk, %d stok, avg Rp%.0f\n",
            s.CategoryName, s.ProductCount, s.TotalStock, s.AvgPrice)
    }
}

Ringkasan #

  • GORM ideal untuk CRUD standar dan relasi — gunakan raw SQL untuk query kompleks atau performa kritis.
  • gorm.Model menyertakan ID, CreatedAt, UpdatedAt, DeletedAt (soft delete) secara otomatis.
  • AutoMigrate aman untuk production — hanya menambahkan, tidak menghapus kolom yang ada.
  • db.Create(&p) auto-set ID, CreatedAt, UpdatedAt setelah berhasil.
  • db.First mengembalikan gorm.ErrRecordNotFound jika tidak ada — cek dengan errors.Is.
  • db.Save update jika ID > 0, insert jika ID = 0; gunakan db.Updates untuk update field tertentu saja.
  • Preload untuk eager loading association — hindari N+1 query; gunakan clause.Associations untuk semua.
  • Hooks (BeforeCreate, AfterUpdate, dll) untuk logika lifecycle — kembalikan error untuk membatalkan operasi.
  • Scopes untuk query condition yang reusable — db.Scopes(Active, InStock, Paginate(1, 10)).
  • db.Transaction(func(tx *gorm.DB) error {...}) — auto commit/rollback berdasarkan return value.
  • Soft delete otomatis jika model punya field DeletedAt gorm.DeletedAt — gunakan db.Unscoped() untuk akses data yang terhapus.

← Sebelumnya: PostgreSQL   Berikutnya: MongoDB →

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