Memahami Dasar Optimasi GORM
Membuat aplikasi Go dengan ORM seperti GORM sering kali membuat pengembang mudah menghasilkan query yang lengkap, tetapi belum tentu efisien. Query yang mengambil seluruh kolom tabel atau melakukan eager loading pada relasi besar secara default bisa merusak latensi API, terutama saat data bertumbuh. Pada bagian ini kita fokus pada pendekatan praktis: seleksi kolom spesifik, pemanfaatan Preload atau Joins yang disesuaikan, pemakaian Scopes untuk filter dinamis, serta kapan waktunya turun ke raw SQL untuk kebutuhan khusus.
Prinsip pertama adalah ambil hanya kebutuhan. GORM memanggil semua kolom secara default, sehingga data yang tidak diperlukan membebani jaringan dan memori aplikasi. Selanjutnya, pahami perbedaan mekanisme pemuatan relasi sebelum menentukan strategi. Akhirnya, evaluasi setiap query dengan benchmarking sederhana agar perbaikan tidak hanya terasa subjektif.
Select Kolom Spesifik untuk Kurangi Payload
Query yang mengambil seluruh kolom (misalnya SELECT *) bisa mengirim data besar yang tidak dipakai. GORM mendukung chaining Select untuk memilih kolom yang relevan secara eksplisit.
var users []UserDTO // DTO hanya menampung nama dan email saja
db.Model(&User{}).
Select("users.id, users.name, users.email").
Where("active = ?", true).
Scan(&users)Dalam contoh di atas kita membatasi kolom yang diambil ke id, name, dan email. Hindari memanggil struct domain langsung tanpa Select, karena GORM otomatis membaca semua field yang terdaftar. Skenario lain melibatkan subquery atau agregasi ringan untuk kolom khusus. Misalnya:
db.Model(&Order{}).
Select("user_id, SUM(amount) as total").
Where("created_at >= ?", startOfMonth).
Group("user_id").
Scan(&monthlySpend)Memilih kolom dengan jelas juga memudahkan debug karena Anda tahu persis data apa yang dikirimkan ke layer aplikasi berikutnya.
Preload vs Joins: Kapan Menggunakan Masing-masing
GORM menyediakan dua cara utama untuk memuat relasi: Preload (eager loading terpisah) dan Joins (query gabungan). Memahami perbedaan adalah kunci performa.
Preload
Preload menjalankan query terpisah untuk relasi. Ini berguna jika relasi relatif kecil atau data utama jauh lebih besar dari relasinya. Karena GORM akan mengeluarkan dua query (satu untuk entitas utama, satu untuk relasi), Anda bisa memanfaatkan indeks berbeda tanpa harus memanipulasi JOIN yang kompleks.
Contoh optimal:
db.Preload("Orders", "status = ?", "completed").
Where("users.active = ?", true).
Find(&users)Preload cocok ketika jumlah relasi tidak terlalu besar dan Anda bisa memecah eksekusi untuk menghindari duplication data yang sering muncul pada hasil JOIN.
Join
Sementara Join berguna jika Anda ingin memfilter berdasarkan kolom relasi atau menghasilkan agregasi dalam satu query. Join bisa lebih efisien bila menghindari multiple round-trip database.
db.Model(&User{}).
Select("users.id, users.name, COUNT(orders.id) as order_count").
Joins("LEFT JOIN orders ON orders.user_id = users.id").
Group("users.id").
Having("COUNT(orders.id) > 5").
Scan(&report)Namun, perhatikan duplication jika relasi 1:N karena hasil query bisa mengembalikan baris duplikat untuk entitas utama. Gunakan Distinct atau agregasi untuk menghindari data ganda. Jangan gunakan Joins jika Anda tidak benar-benar perlu data relasi langsung dalam query utama; Preload dapat lebih mudah dibaca.
Scopes untuk Filter Dinamis yang Reusable
Untuk menjaga maintainability, gunakan Scopes GORM untuk filter dinamis dan rangkaian query yang sering digunakan.
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("active = ?", true)
}
func WithOrdersAfter(date time.Time) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("Orders", "created_at >= ?", date)
}
}
// Penggunaan
var vipUsers []User
Db.Scopes(ActiveUsers, WithOrdersAfter(time.Now().AddDate(0, -1, 0))).Find(&vipUsers)Scopes membuat query kombinasi bisa disusun ulang tanpa menulis ulang logika filter. Saat kondisi berubah berdasarkan parameter user (misalnya filter data per role atau area), Anda cukup membangun list scope dan memanggil db.Scopes(scope1, scope2). Ini juga memfasilitasi pengujian, karena setiap scope bisa diuji secara terpisah.
Kapan Harus Menggunakan Raw SQL
GORM hebat untuk CRUD, tapi ada situasi di mana query terlalu rumit atau membutuhkan fitur database yang tidak didukung langsung, seperti CTE kompleks, LATERAL JOIN, atau optimasi agregasi khusus.
Gunakan db.Raw untuk melakukan query tersebut, tapi jangan lupa tes parameter binding untuk menghindari SQL injection. Contoh:
db.Raw(`WITH recent_orders AS (
SELECT user_id, MAX(created_at) as last_order
FROM orders
GROUP BY user_id
)
SELECT users.id, users.name, recent_orders.last_order
FROM users
JOIN recent_orders ON recent_orders.user_id = users.id
WHERE users.status = ?`, "active").Scan(&result)Pendekatan ini memberikan fleksibilitas penuh terhadap struktur query. Namun, trade-off-nya adalah Anda kehilangan kelebihan GORM seperti auto-mapping relasi, sehingga Anda perlu mengelola scanning hasil sendiri. Komposisi raw SQL harus disimpan di satu tempat agar mudah dipelihara.
Benchmarking: Membandingkan Query Default vs Optimized
Untuk membuktikan perbaikan performa, lakukan benchmarking sederhana. Misalnya, bandingkan query default yang memuat seluruh entitas dan relasi terhadap versi yang sudah dioptimasi.
Siapkan dataset representatif: Gunakan data mirip produksi (misalnya 10k user dan 50k order).
Jalankan query default: Catat waktu dengan
time.Now()sebelum dan sesudah query.Optimasi query: terapkan
Selectkolom spesifik,Scopes, atauJoinsterkontrol.Bandingkan hasil: lihat perbedaan waktu, jumlah baris, dan payload JSON.
Contoh:
start := time.Now()
db.Preload("Orders").Find(&users)
durationDefault := time.Since(start)
start = time.Now()
db.Select("id, name").Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Select("id, user_id, amount")
}).Find(&users)
durationOptimized := time.Since(start)
fmt.Println("Default:", durationDefault)
fmt.Println("Optimized:", durationOptimized)Catat bahwa hasil benchmark sebaiknya diulang beberapa kali, dan gunakan profiling database (misalnya EXPLAIN ANALYZE) jika hasil tidak sejalan dengan ekspektasi. Jika optimasi tidak memberikan peningkatan signifikan, pertimbangkan kompleksitas tambahan sebelum mengadopsi.
Kesimpulan
Optimasi query GORM terdiri dari beberapa langkah konkret: mengunci kolom lewat Select, memahami kapan harus menggunakan Preload atau Join, membangun logika query dengan Scopes, dan kembali ke raw SQL saat ORM tidak cukup fleksibel. Sertakan benchmarking sederhana untuk memastikan perubahan benar-benar menguntungkan, serta dokumentasikan trade-off (misal kompleksitas raw SQL vs kemudahan GORM). Dengan pendekatan seperti ini, aplikasi Go Anda bisa tetap responsif sekaligus mudah dirawat.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!