Avoid ORM #

ORM menjanjikan produktivitas: tidak perlu menulis SQL, relasi antar tabel dikelola otomatis, dan kode terlihat bersih dengan method chaining. Di awal proyek, semua ini terasa benar. Masalah mulai muncul ketika sistem tumbuh — ketika tabel punya jutaan baris, ketika query butuh JOIN tiga tabel dengan kondisi kompleks, ketika laporan analytics butuh window function, atau ketika N+1 tiba-tiba muncul di production dan developer butuh berjam-jam untuk menyadarinya karena SQL yang dihasilkan ORM tidak pernah dilihat. Judul artikel ini bukan “jangan pakai ORM sama sekali” — ORM punya tempat yang tepat. Yang perlu dihindari adalah penggunaan ORM secara refleks untuk semua query, tanpa pernah melihat SQL yang dihasilkan, tanpa memahami trade-off yang dibawa. Artikel ini membahas masalah konkret yang ditimbulkan ORM, perbandingan GORM vs sqlx di Go, pola hybrid yang scalable, dan panduan kapan harus turun ke raw SQL.

Hidden Query: ORM Menyembunyikan Biaya yang Nyata #

Masalah terbesar ORM bukan pada SQL yang buruk secara teknis — tapi pada fakta bahwa developer tidak tahu SQL apa yang dieksekusi. Setiap kali ini terjadi, ada potensi performa tersembunyi yang menunggu untuk menjadi insiden di production.

Contoh Hidden Query di GORM #

// Kode Go yang terlihat bersih
var users []User
db.Preload("Orders").Preload("Orders.Items").Find(&users)

// SQL yang sebenarnya dieksekusi (dengan 100 user):
// Query 1: SELECT * FROM users
// Query 2: SELECT * FROM orders WHERE user_id IN (1,2,3,...,100)
// Query 3: SELECT * FROM order_items WHERE order_id IN (...)
//
// Ini OK — Preload yang benar menggunakan batch query
// Tapi perhatikan Preload tanpa scoping:

db.Preload("Orders").Find(&users)
// Query 2 di atas tidak ada LIMIT atau filter status
// Jika user punya 500 order masing-masing:
// → Query 2 mengembalikan 50.000 baris ke memory aplikasi
// → Transfer network: mungkin puluhan MB
// → Semua masuk ke memory sebagai slice Go
// Developer tidak sadar karena kodenya terlihat "satu baris"

SELECT * yang ORM Lakukan Secara Default #

// GORM: Find selalu SELECT *
var products []Product
db.Where("category_id = ?", categoryID).Find(&products)

// SQL yang dihasilkan:
// SELECT * FROM products WHERE category_id = 5
//
// Tabel products punya kolom:
// id, name, description (TEXT 5KB), price, stock, weight,
// dimensions (JSON), metadata (JSON), image_urls (JSON),
// created_at, updated_at, deleted_at
//
// API hanya butuh: id, name, price
// Tapi kita menarik semua kolom termasuk description 5KB per produk
// Untuk 200 produk: 200 × 5KB = 1MB data tak berguna per request

// Solusi GORM: gunakan Select() eksplisit
db.Select("id, name, price").Where("category_id = ?", categoryID).Find(&products)
// SQL: SELECT id, name, price FROM products WHERE category_id = 5
// Lebih baik, tapi banyak developer lupa melakukan ini

Query yang ORM Hasilkan tapi Tidak Bisa Kamu Kontrol #

// Soft delete di GORM: otomatis menambahkan WHERE deleted_at IS NULL
// di SEMUA query
var orders []Order
db.Where("user_id = ?", userID).Find(&orders)
// SQL: SELECT * FROM orders WHERE user_id = 42 AND deleted_at IS NULL

// Masalah: jika composite index kamu adalah (user_id, status, created_at)
// tapi GORM menambahkan deleted_at IS NULL di belakang,
// query planner mungkin tidak menggunakan index tersebut secara optimal
// karena urutan kolom dalam kondisi WHERE berbeda dari urutan di index

// Kamu tidak bisa mengontrol kondisi yang GORM tambahkan secara otomatis
// tanpa workaround yang mengotori kode

// Contoh GORM yang menghasilkan kondisi tidak index-friendly:
db.Where("DATE(created_at) = ?", today).Find(&orders)
// SQL: ... WHERE DATE(created_at) = '2026-04-18' AND deleted_at IS NULL
// DATE() di kolom → index tidak terpakai (sudah dibahas di SQL Function Overuse)
// Dan kondisi ini sering muncul karena ORM memudahkan penulisan yang "terlihat wajar"

Lima Masalah Konkret ORM di Production #

Masalah 1: N+1 yang Tersembunyi di Balik Kode yang Rapi #

// Kode yang terlihat bersih, tapi berbahaya
func GetUserList(ctx context.Context) ([]UserResponse, error) {
    var users []User
    db.Find(&users)  // 1 query

    var result []UserResponse
    for _, u := range users {
        // Akses u.Profile memicu query baru jika belum di-load!
        // GORM lazy load: satu query per user
        result = append(result, UserResponse{
            Name:    u.Name,
            City:    u.Profile.City,    // ← hidden query: SELECT * FROM profiles WHERE user_id = ?
            Country: u.Profile.Country, // ← tidak ada query tambahan (sudah di-load)
        })
    }
    return result, nil
}
// Untuk 1.000 user: 1 + 1.000 = 1.001 query
// Developer mungkin tidak sadar sampai melihat slow query log

// GORM dengan Preload (lebih baik):
db.Preload("Profile").Find(&users)
// 1 query users + 1 batch query profiles = 2 query total
// Tapi tetap SELECT * dari kedua tabel

Masalah 2: Overfetching yang Terakumulasi #

// Contoh nyata: endpoint list produk dengan ORM
func GetProducts(categoryID int) []ProductResponse {
    var products []Product
    db.Where("category_id = ?", categoryID).
       Where("deleted_at IS NULL").
       Find(&products)
    // Menarik SEMUA kolom termasuk: description (TEXT 5KB),
    // metadata (JSON 2KB), image_urls (JSON 1KB), dll.

    var responses []ProductResponse
    for _, p := range products {
        responses = append(responses, ProductResponse{
            ID:    p.ID,
            Name:  p.Name,
            Price: p.Price,
            // Hanya butuh 3 kolom dari 15 yang di-load!
        })
    }
    return responses
}

// Biaya nyata untuk 200 produk:
// Data yang ditarik: 200 × (5KB + 2KB + 1KB + ...) = ~2MB
// Data yang dipakai: 200 × (8B + 200B + 8B) = ~44KB
// Efisiensi: ~2%
// Sisa 98% data dibuang setelah mapping ke DTO

Masalah 3: Query Kompleks yang ORM Ekspresikan Buruk #

// Query yang perlu ditulis: ambil produk terlaris per kategori
// (window function: RANK() OVER PARTITION BY)

// Dengan GORM — tidak bisa dilakukan dengan bersih:
// Harus pakai Raw() yang mengalahkan tujuan ORM
var result []struct {
    CategoryID int
    ProductID  int
    SalesRank  int
}
db.Raw(`
    SELECT category_id, id AS product_id,
           RANK() OVER (PARTITION BY category_id ORDER BY total_sold DESC) AS sales_rank
    FROM products
    WHERE status = 'active'
`, ).Scan(&result)
// Kamu menulis SQL mentah di dalam ORM
// Tidak ada manfaat ORM di sini, hanya overhead abstraksi

// Lebih baik langsung dengan sqlx:
sqlx.SelectContext(ctx, db, &result, `
    SELECT category_id, id AS product_id,
           RANK() OVER (PARTITION BY category_id ORDER BY total_sold DESC) AS sales_rank
    FROM products
    WHERE status = 'active'
`)
// Lebih jelas, tidak ada layer ORM yang tidak berguna

Masalah 4: Magic Condition yang Mengganggu Index #

// GORM soft delete menambahkan kondisi otomatis
// yang bisa mengganggu index yang sudah dirancang dengan hati-hati

// Index yang dirancang untuk query pagination:
// CREATE INDEX idx_orders ON orders (user_id, status, created_at DESC)

// Query GORM yang dihasilkan:
// SELECT * FROM orders
// WHERE user_id = 42 AND status = 'paid'
// AND deleted_at IS NULL   ← ditambahkan GORM
// ORDER BY created_at DESC
// LIMIT 20

// Masalah: kondisi deleted_at IS NULL menginterupsi urutan kolom
// yang optimal untuk index (user_id, status, created_at)
// Index mungkin masih terpakai tapi tidak seoptimal jika
// deleted_at juga ada di index atau tidak ada kondisi tersebut

Masalah 5: ORM Mendorong Pengabaian EXPLAIN #

Ini adalah masalah budaya, bukan teknis semata:

Pola yang terjadi di tim yang terlalu bergantung ORM:
──────────────────────────────────────────────────────────────
  Developer menulis kode ORM
    → "Terlihat bersih, langsung commit"
    → Tidak ada kebiasaan melihat SQL yang dihasilkan
    → Tidak ada kebiasaan menjalankan EXPLAIN
    → Tidak ada review apakah index terpakai

  Masalah performa muncul 3 bulan kemudian
    → "Database tiba-tiba lambat"
    → Investigasi: ditemukan query tanpa index, N+1, SELECT *
    → Solusi: refactor besar-besaran yang bisa dicegah dari awal

  Dengan raw SQL / sqlx:
    Developer menulis SQL eksplisit
    → Lebih mudah untuk menjalankan EXPLAIN di development
    → Review SQL menjadi bagian natural dari code review
    → Masalah performa terdeteksi lebih awal
──────────────────────────────────────────────────────────────

GORM vs sqlx di Go: Perbandingan Konkret #

Di ekosistem Go, pilihan yang paling umum adalah antara GORM (ORM penuh) dan sqlx (thin wrapper di atas database/sql). Berikut perbandingan untuk kasus yang sama.

Kasus: Ambil Daftar Order dengan Data User #

// === Pendekatan GORM ===
type Order struct {
    gorm.Model
    UserID  uint
    Total   float64
    Status  string
    User    User `gorm:"foreignKey:UserID"`
}

func GetOrdersGORM(userID uint, status string) ([]Order, error) {
    var orders []Order
    result := db.Preload("User").
        Where("user_id = ? AND status = ?", userID, status).
        Order("created_at DESC").
        Limit(20).
        Find(&orders)
    return orders, result.Error
}
// SQL yang dihasilkan:
// SELECT * FROM orders WHERE user_id = 42 AND status = 'paid'
//   AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 20
// SELECT * FROM users WHERE id IN (42)  ← batch Preload
//
// Masalah tersembunyi:
// - SELECT * dari orders (semua kolom) + SELECT * dari users
// - deleted_at IS NULL ditambahkan otomatis
// - Tidak mudah untuk pilih kolom spesifik

// === Pendekatan sqlx ===
type OrderWithUser struct {
    OrderID   int64   `db:"order_id"`
    Total     float64 `db:"total"`
    Status    string  `db:"status"`
    CreatedAt time.Time `db:"created_at"`
    UserName  string  `db:"user_name"`
    UserEmail string  `db:"user_email"`
}

func GetOrdersSQLX(ctx context.Context, db *sqlx.DB, userID int64, status string) ([]OrderWithUser, error) {
    var orders []OrderWithUser
    err := db.SelectContext(ctx, &orders, `
        SELECT
            o.id          AS order_id,
            o.total,
            o.status,
            o.created_at,
            u.name        AS user_name,
            u.email       AS user_email
        FROM orders o
        JOIN users u ON o.user_id = u.id
        WHERE o.user_id = ?
          AND o.status   = ?
          AND o.deleted_at IS NULL
        ORDER BY o.created_at DESC
        LIMIT 20
    `, userID, status)
    return orders, err
}
// SQL yang dieksekusi: persis seperti yang ditulis
// Hanya kolom yang dibutuhkan yang ditarik
// JOIN dalam satu query — tidak ada query kedua untuk user
// Kondisi eksplisit dan terkontrol penuh

Kapan GORM Masih Masuk Akal #

// GORM tetap berguna untuk operasi CRUD sederhana
// yang tidak butuh kolom spesifik atau query kompleks

// Contoh yang cocok untuk GORM:
func CreateUser(user *User) error {
    return db.Create(user).Error  // INSERT sederhana
}

func UpdateUserStatus(userID uint, status string) error {
    return db.Model(&User{}).
        Where("id = ?", userID).
        Update("status", status).Error
    // UPDATE users SET status = ?, updated_at = ? WHERE id = ?
}

func DeleteUser(userID uint) error {
    return db.Delete(&User{}, userID).Error
    // UPDATE users SET deleted_at = ? WHERE id = ?  (soft delete)
}

// Untuk operasi di atas, GORM menyederhanakan kode
// dan menghasilkan query yang memadai

Pola Hybrid: Repository Layer yang Tepat #

Solusi terbaik bukan memilih antara “hanya ORM” atau “tidak ada ORM sama sekali” — tapi mendesain repository layer yang menggunakan tool yang tepat untuk setiap jenis operasi.

Struktur Repository yang Disarankan #

// Repository yang menggunakan GORM untuk write, sqlx untuk read kompleks
type OrderRepository struct {
    gormDB *gorm.DB    // untuk write operations
    sqlxDB *sqlx.DB    // untuk read operations yang butuh kontrol
}

// WRITE: gunakan GORM — sederhana dan aman
func (r *OrderRepository) Create(ctx context.Context, order *Order) error {
    return r.gormDB.WithContext(ctx).Create(order).Error
}

func (r *OrderRepository) UpdateStatus(ctx context.Context, orderID int64, status string) error {
    return r.gormDB.WithContext(ctx).
        Model(&Order{}).
        Where("id = ?", orderID).
        Update("status", status).Error
}

// READ SEDERHANA: bisa GORM
func (r *OrderRepository) FindByID(ctx context.Context, id int64) (*Order, error) {
    var order Order
    err := r.gormDB.WithContext(ctx).First(&order, id).Error
    return &order, err
}

// READ KOMPLEKS: gunakan sqlx dengan SQL eksplisit
func (r *OrderRepository) GetUserOrderSummary(ctx context.Context, userID int64) ([]OrderSummary, error) {
    var summaries []OrderSummary
    err := r.sqlxDB.SelectContext(ctx, &summaries, `
        SELECT
            o.id,
            o.status,
            o.total,
            o.created_at,
            COUNT(oi.id)         AS item_count,
            SUM(oi.qty)          AS total_qty,
            p.name               AS first_product_name
        FROM orders o
        JOIN order_items oi ON oi.order_id = o.id
        JOIN products p ON p.id = oi.product_id
        WHERE o.user_id = ?
          AND o.deleted_at IS NULL
        GROUP BY o.id, o.status, o.total, o.created_at, p.name
        ORDER BY o.created_at DESC
        LIMIT 10
    `, userID)
    return summaries, err
}

// READ DENGAN PAGINATION DAN FILTER DINAMIS
func (r *OrderRepository) List(ctx context.Context, params ListOrderParams) ([]OrderListItem, error) {
    // Bangun query secara aman dengan parameter
    query := `
        SELECT o.id, o.total, o.status, o.created_at, u.name AS user_name
        FROM orders o
        JOIN users u ON o.user_id = u.id
        WHERE o.deleted_at IS NULL
    `
    args := []interface{}{}

    if params.Status != "" {
        query += " AND o.status = ?"
        args = append(args, params.Status)
    }
    if params.UserID != 0 {
        query += " AND o.user_id = ?"
        args = append(args, params.UserID)
    }
    if !params.StartDate.IsZero() {
        query += " AND o.created_at >= ?"
        args = append(args, params.StartDate)
    }

    query += " ORDER BY o.created_at DESC LIMIT ? OFFSET ?"
    args = append(args, params.Limit, params.Offset)

    var items []OrderListItem
    err := r.sqlxDB.SelectContext(ctx, &items, query, args...)
    return items, err
}

DTO dan Projection: Ambil Hanya yang Dibutuhkan #

// Definisikan struct terpisah untuk setiap kebutuhan read
// Bukan satu struct User/Order yang dipakai untuk semua

// Untuk list endpoint — hanya kolom yang ditampilkan di tabel
type OrderListItem struct {
    ID        int64     `db:"id"`
    Total     float64   `db:"total"`
    Status    string    `db:"status"`
    CreatedAt time.Time `db:"created_at"`
    UserName  string    `db:"user_name"`
}

// Untuk detail endpoint — lebih banyak kolom
type OrderDetail struct {
    ID           int64          `db:"id"`
    Total        float64        `db:"total"`
    Status       string         `db:"status"`
    CreatedAt    time.Time      `db:"created_at"`
    UpdatedAt    time.Time      `db:"updated_at"`
    UserName     string         `db:"user_name"`
    UserEmail    string         `db:"user_email"`
    ShippingAddr string         `db:"shipping_addr"`
}

// Untuk analytics/report — kolom agregasi
type OrderAnalytics struct {
    Date         string  `db:"order_date"`
    TotalRevenue float64 `db:"total_revenue"`
    OrderCount   int     `db:"order_count"`
    AvgOrderSize float64 `db:"avg_order_size"`
}

// Setiap struct hanya punya field yang benar-benar dibutuhkan
// Query SQL bisa ditulis dengan SELECT yang persis sesuai struct
// Tidak ada overfetching, tidak ada mapping yang membuang data

Mengaktifkan SQL Logging sebagai Kebiasaan #

Salah satu perubahan budaya yang paling penting adalah membiasakan melihat SQL yang sebenarnya dieksekusi, terutama selama development.

// GORM: aktifkan SQL logging di development
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: logger.Default.LogMode(logger.Info),
    // LogMode(logger.Info) → log semua query
    // LogMode(logger.Warn) → hanya log query lambat
    // LogMode(logger.Silent) → tidak ada log (production default)
})

// sqlx dengan custom logger wrapper
type LoggingDB struct {
    *sqlx.DB
    log *zap.Logger
}

func (ldb *LoggingDB) SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
    start := time.Now()
    err := ldb.DB.SelectContext(ctx, dest, query, args...)
    ldb.log.Debug("sql",
        zap.String("query", query),
        zap.Any("args", args),
        zap.Duration("elapsed", time.Since(start)),
        zap.Error(err),
    )
    return err
}

// Workflow yang disarankan:
// 1. Tulis kode ORM / sqlx
// 2. Jalankan dengan logging aktif
// 3. Baca SQL yang dihasilkan
// 4. Jalankan EXPLAIN untuk query penting
// 5. Baru commit ke repo

Panduan: Kapan ORM, Kapan Raw SQL #

Decision matrix: ORM vs Raw SQL
──────────────────────────────────────────────────────────────────────
  Operasi                          │ ORM    │ Raw SQL/sqlx
──────────────────────────────────────────────────────────────────────
  INSERT baris tunggal             │ ✓ ORM  │ Bisa keduanya
  UPDATE satu atau beberapa kolom  │ ✓ ORM  │ Bisa keduanya
  DELETE / Soft delete             │ ✓ ORM  │ Bisa keduanya
  SELECT by primary key            │ ✓ ORM  │ Bisa keduanya
  SELECT dengan WHERE sederhana    │ ~ ORM* │ ✓ sqlx (lebih kontrol)
  SELECT dengan JOIN               │ ✗ ORM  │ ✓ sqlx
  SELECT dengan kolom spesifik     │ ✗ ORM  │ ✓ sqlx
  Aggregation (SUM, COUNT, AVG)    │ ✗ ORM  │ ✓ sqlx
  Window function                  │ ✗ ORM  │ ✓ sqlx
  Subquery kompleks                │ ✗ ORM  │ ✓ sqlx
  CTE (Common Table Expression)    │ ✗ ORM  │ ✓ sqlx
  Bulk INSERT (multi-row)          │ ✗ ORM  │ ✓ sqlx
  Upsert (ON DUPLICATE KEY)        │ ~ ORM* │ ✓ sqlx
  Pagination dengan filter dinamis │ ✗ ORM  │ ✓ sqlx
  Analytics / reporting query      │ ✗ ORM  │ ✓ sqlx
──────────────────────────────────────────────────────────────────────
  * ORM bisa dipakai tapi perlu verifikasi SQL yang dihasilkan

Prinsip umum:
  Jika query bisa ditulis dengan mudah di ORM DAN kamu sudah
  memverifikasi SQL-nya optimal: gunakan ORM.

  Jika query membutuhkan kontrol kolom, JOIN, aggregation,
  atau SQL yang ORM hasilkan tidak optimal: gunakan sqlx.
──────────────────────────────────────────────────────────────────────

Anti-Pattern yang Harus Dihindari #

// ✗ Anti-pattern 1: Find() tanpa Select() di tabel dengan kolom besar
db.Where("category_id = ?", id).Find(&products)
// → SELECT * termasuk kolom description, metadata, dll.
// ✓ Solusi: db.Select("id, name, price").Where(...).Find(&products)
// atau gunakan sqlx dengan SELECT eksplisit

// ✗ Anti-pattern 2: akses relasi tanpa Preload (lazy loading N+1)
for _, u := range users {
    fmt.Println(u.Orders)  // trigger query per user
}
// ✓ Solusi: db.Preload("Orders").Find(&users)
// atau lebih baik: sqlx dengan JOIN atau batch IN

// ✗ Anti-pattern 3: ORM untuk query analytics yang butuh aggregasi
db.Raw("SELECT DATE(created_at) as date, SUM(total) as revenue, COUNT(*) as count FROM orders GROUP BY DATE(created_at)").Scan(&result)
// Pakai Raw() → manfaat ORM hilang, tapi masih ada overhead abstraksi
// ✓ Solusi: langsung gunakan sqlx

// ✗ Anti-pattern 4: tidak pernah mengaktifkan SQL logging
// Developer tidak tahu query apa yang dieksekusi
// ✓ Solusi: aktifkan logger.Info di GORM atau buat wrapper logging di sqlx
// Jadikan ini mandatory di development environment

// ✗ Anti-pattern 5: mapping langsung ke entity besar untuk response API
var orders []Order
db.Find(&orders)  // menarik semua kolom dari semua relasi
json.NewEncoder(w).Encode(orders)
// ✓ Solusi: definisikan DTO terpisah untuk setiap use case,
// SELECT hanya kolom yang ada di DTO tersebut

Checklist Penggunaan ORM yang Sehat #

SAAT MENULIS QUERY ORM:
  □ Sudah lihat SQL yang dihasilkan? (aktifkan logging)
  □ Tidak ada SELECT * untuk tabel dengan banyak kolom besar?
  □ Tidak ada akses relasi tanpa Preload?
  □ Query sudah diverifikasi dengan EXPLAIN di development?

SAAT CODE REVIEW:
  □ Query ORM punya Select() eksplisit jika table punya kolom besar?
  □ Preload digunakan dengan benar (bukan lazy loading di loop)?
  □ Query kompleks (JOIN, aggregation, window function) menggunakan raw SQL/sqlx?
  □ Ada DTO terpisah untuk setiap use case read?

ARSITEKTUR:
  □ Repository layer memisahkan ORM (write) dari sqlx (read kompleks)?
  □ SQL logging aktif di development?
  □ Ada query count monitoring per request di production?
  □ Developer tim memahami SQL yang dihasilkan ORM (bukan hanya ORM syntax)?

Ringkasan #

  • ORM menyembunyikan SQL — ini masalah, bukan fitur — developer yang tidak tahu SQL apa yang dieksekusi tidak bisa memastikan apakah index terpakai, apakah ada N+1, atau apakah ada overfetching. Selalu aktifkan SQL logging dan baca outputnya.
  • SELECT * adalah default ORM yang berbahaya — tabel dengan kolom TEXT, JSON, atau BLOB yang di-Find() tanpa Select() akan menarik semua data tersebut ke memory, meskipun API hanya butuh dua atau tiga kolom. Selalu specify kolom yang dibutuhkan.
  • ORM untuk write, sqlx untuk read kompleks — pola hybrid ini memberikan produktivitas ORM untuk INSERT/UPDATE/DELETE sederhana, sekaligus kontrol penuh SQL untuk query yang butuh JOIN, aggregation, window function, atau kolom spesifik.
  • DTO/projection per use case menghilangkan overfetching — definisikan struct yang berbeda untuk list endpoint, detail endpoint, dan analytics. Setiap struct hanya punya field yang benar-benar dibutuhkan, dan query SELECT mengikuti struct tersebut.
  • Preload ORM harus diverifikasi — Preload yang benar menggunakan batch query (2-3 query total), tapi Preload tanpa scoping bisa menarik ribuan baris yang tidak diperlukan. Selalu verifikasi SQL yang dihasilkan.
  • Lazy loading adalah anti-pattern — akses relasi ORM tanpa Preload eksplisit akan memicu satu query per baris (N+1) yang tersembunyi di balik kode yang terlihat bersih.
  • ORM untuk query kompleks berarti Raw() — kalahkan tujuan ORM — jika kamu butuh Raw() untuk aggregation, window function, atau CTE, lebih baik langsung gunakan sqlx yang lebih eksplisit dan lebih mudah di-maintain.
  • Budaya melihat SQL adalah kunci — tanpa kebiasaan melihat dan memahami SQL yang dihasilkan, masalah performa ORM akan selalu muncul di production, bukan di development.

← Sebelumnya: Full-Text Index   Berikutnya: Index with Sort →

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