Package database/sql memberi kendali penuh, tetapi harganya terasa pada setiap query: hasil baris harus disalin satu per satu ke field struct lewat Scan. Tiga belas baris kode hanya untuk membaca satu tabel kecil bukan hal aneh, dan kesalahan urutan kolom baru ketahuan saat program berjalan. Ekosistem Go menjawab kejenuhan tersebut dari beberapa arah — sqlx yang memperpendek kode, sqlc yang meng-generate kode, dan GORM yang memetakan struct secara penuh. Chapter ini membandingkan keempat pendekatan supaya kita bisa memilih secara sadar, bukan karena kebiasaan.
Kerja Manual dengan database/sql #
Pola lengkap membaca banyak baris dengan database/sql tampak seperti berikut. Kode ini membutuhkan database aktif untuk dijalankan; versi utuhnya dipraktikkan di proyek REST API dengan PostgreSQL
:
rows, err := db.QueryContext(ctx, "SELECT id, nama, email FROM pengguna WHERE aktif = $1", true)
if err != nil {
return err
}
defer rows.Close()
semua := []Pengguna{}
for rows.Next() {
var p Pengguna
if err := rows.Scan(&p.ID, &p.Nama, &p.Email); err != nil {
return err
}
semua = append(semua, p)
}
if err := rows.Err(); err != nil {
return err
}Perhatikan sumber repetisi: setiap kolom disebut dua kali — sekali di teks SQL, sekali sebagai argumen Scan — dan urutan keduanya harus selalu sinkron. Query dengan sepuluh kolom menghasilkan daftar argumen yang sama panjangnya.
sqlx: Get dan Select Langsung ke Struct #
Package sqlx bukan pengganti database/sql, melainkan lapisan tipis di atasnya: sqlx.NewDb(db, "pgx") membungkus *sql.DB yang sudah kita miliki, dan seluruh perilaku pool tetap dikelola database/sql. Yang berubah hanya pemetaan hasil. Struct kini diberi struct tag db:"kolom":
type Pengguna struct {
ID int64 `db:"id"`
Nama string `db:"nama"`
Email string `db:"email"`
}
dbx := sqlx.NewDb(db, "pgx")
var p Pengguna
err = dbx.GetContext(ctx, &p,
"SELECT id, nama, email FROM pengguna WHERE id = $1", 1)
var semua []Pengguna
err = dbx.SelectContext(ctx, &semua,
"SELECT id, nama, email FROM pengguna ORDER BY id")Method GetContext mengisi struct untuk tepat satu baris — dan mengembalikan sql.ErrNoRows bila baris tidak ditemukan. Method SelectContext mengisi slice struct untuk banyak baris. Kode rows.Next, rows.Scan, dan rows.Close hilang dari aplikasi, tetapi sqlx tetap menjalankan pola tersebut di belakang layar.
sqlx juga menyediakan parameter bernama. Placeholder ditulis :nama dan nilainya diambil dari field struct atau key map:
_, err = dbx.NamedExecContext(ctx,
`INSERT INTO pengguna (nama, email) VALUES (:nama, :email)`,
Pengguna{Nama: "Budi", Email: "[email protected]"})Cara Kerja Mapping di Balik Layar #
Bagaimana sqlx tahu kolom nama harus masuk ke field Nama? Package reflect membaca struct tag saat runtime, lalu nilai kolom disalin ke field yang cocok. Berkas contoh.go pada bagian bawah halaman mendemonstrasikan inti mekanisme tersebut — baris data dimodelkan sebagai map[string]any sehingga program berjalan tanpa database:
package main
import (
"fmt"
"reflect"
)
type Pengguna struct {
ID int64 `db:"id"`
Nama string `db:"nama"`
Email string `db:"email"`
}
// mapBaris menyalin nilai satu baris hasil query ke field struct
// berdasarkan struct tag db:"..." — versi sederhana dari
// mekanisme mapping sqlx Get dan Select.
func mapBaris(baris map[string]any, dst any) error {
v := reflect.ValueOf(dst).Elem()
t := v.Type()
for i := range t.NumField() {
tag := t.Field(i).Tag.Get("db")
nilai, ada := baris[tag]
if !ada {
continue
}
switch v.Field(i).Kind() {
case reflect.Int64:
n, ok := nilai.(int64)
if !ok {
return fmt.Errorf("kolom %s bukan int64", tag)
}
v.Field(i).SetInt(n)
case reflect.String:
s, ok := nilai.(string)
if !ok {
return fmt.Errorf("kolom %s bukan string", tag)
}
v.Field(i).SetString(s)
}
}
return nil
}
func main() {
barisan := []map[string]any{
{"id": int64(1), "nama": "Budi", "email": "[email protected]"},
{"id": int64(2), "nama": "Siti", "email": "[email protected]"},
}
for _, baris := range barisan {
var p Pengguna
if err := mapBaris(baris, &p); err != nil {
fmt.Println("gagal memetakan:", err)
return
}
fmt.Printf("%d %s <%s>\n", p.ID, p.Nama, p.Email)
}
}Output yang Dihasilkan:
1 Budi <[email protected]>
2 Siti <[email protected]>Mari kita telaah baris demi baris program di atas:
- Baris 8-12 (
type Pengguna): struct ditandai struct tagdb:"..."— kontrak nama kolom untuk setiap field. - Baris 18-19 (
reflect.ValueOf(...).Elem()): membuka nilai yang ditunjuk pointerdstsupaya field-nya bisa dimodifikasi. - Baris 21 (
t.Field(i).Tag.Get("db")): membaca tagdbpada field ke-iuntuk tahu nama kolom yang cocok. - Baris 26-38 (
switch v.Field(i).Kind()): menyalin nilai sesuai jenis field —SetIntuntukint64,SetStringuntukstring, lengkap dengan validasi tipe nilai masukan. - Baris 45-48 (
barisan := ...): dua baris data tiruan; setiap baris berupamapdengan key yang sama dengan nama kolom.
Program ini menyederhanakan apa yang dilakukan sqlx: driver asli membaca nama kolom dari hasil query, mencocokkannya dengan tag, lalu menyalin nilai satu per satu.
stdlib, sqlx, sqlc, dan GORM #
| Alat | Pendekatan | Kekuatan | Kelemahan |
|---|---|---|---|
database/sql | Scan manual | Kendali penuh, tanpa dependensi | Boilerplate panjang, urutan kolom rawan salah |
sqlx | Reflection saat runtime | Mapping otomatis, kode ringkas | Kesalahan mapping baru terlihat saat runtime |
sqlc | Men-generate kode Go dari SQL | Type-safe saat kompilasi | Butuh tools terpisah, SQL ditulis di berkas khusus |
| GORM | ORM penuh | CRUD standar sangat cepat ditulis | Query tersembunyi, mudah tergelincir ke query lambat |
Pilihannya mengikuti kebutuhan: prototipe internal yang mengejar kecepatan sering nyaman dengan GORM; tim yang ingin SQL tetap satu-satunya kebenaran dan diperiksa compiler condong ke sqlc; proyek dengan sedikit query sering cukup dengan sqlx; dan aplikasi dengan query khusus yang kompleks justru paling terbantu oleh Scan manual database/sql.
Apapun lapisan yang kita pilih, langkah pertama tetap sama: membuka koneksi ke database. Chapter berikutnya membahas sql.Open, DSN PostgreSQL, dan kejutan pertama bagi pemula — kenyataan bahwa sql.Open tidak langsung tersambung.
Referensi #
- Dokumentasi package database/sql — antarmuka yang menjadi fondasi semua lapisan di atasnya.
- Tutorial: Accessing a relational database
— tutorial resmi dengan pola
Scanmanual.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.go
Unduh berkaspackage main
import (
"fmt"
"reflect"
)
type Pengguna struct {
ID int64 `db:"id"`
Nama string `db:"nama"`
Email string `db:"email"`
}
// mapBaris menyalin nilai satu baris hasil query ke field struct
// berdasarkan struct tag db:"..." — versi sederhana dari
// mekanisme mapping sqlx Get dan Select.
func mapBaris(baris map[string]any, dst any) error {
v := reflect.ValueOf(dst).Elem()
t := v.Type()
for i := range t.NumField() {
tag := t.Field(i).Tag.Get("db")
nilai, ada := baris[tag]
if !ada {
continue
}
switch v.Field(i).Kind() {
case reflect.Int64:
n, ok := nilai.(int64)
if !ok {
return fmt.Errorf("kolom %s bukan int64", tag)
}
v.Field(i).SetInt(n)
case reflect.String:
s, ok := nilai.(string)
if !ok {
return fmt.Errorf("kolom %s bukan string", tag)
}
v.Field(i).SetString(s)
}
}
return nil
}
func main() {
barisan := []map[string]any{
{"id": int64(1), "nama": "Budi", "email": "[email protected]"},
{"id": int64(2), "nama": "Siti", "email": "[email protected]"},
}
for _, baris := range barisan {
var p Pengguna
if err := mapBaris(baris, &p); err != nil {
fmt.Println("gagal memetakan:", err)
return
}
fmt.Printf("%d %s <%s>\n", p.ID, p.Nama, p.Email)
}
}