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.Modelmenyertakan 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.Firstmengembalikangorm.ErrRecordNotFoundjika tidak ada — cek denganerrors.Is.db.Saveupdate jika ID > 0, insert jika ID = 0; gunakandb.Updatesuntuk update field tertentu saja.- Preload untuk eager loading association — hindari N+1 query; gunakan
clause.Associationsuntuk 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— gunakandb.Unscoped()untuk akses data yang terhapus.