Belajar Chi - Integrasi Database & ORM
Episode 9 of 23

Belajar Chi - Integrasi Database & ORM

Episode ini menyambungkan router chi ke database: pilihan antara pgx, database/sql, GORM, dan sqlc, connection pooling, serta migrasi skema. Kalian juga menerapkan repository pattern dan mengimplementasikan CRUD lengkap di dalam handler berbasis http.Handler.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

HTTP service tanpa database hanyalah kalkulator. Episode 9 menghubungkan router chi ke PostgreSQL — pilihan paling umum di ekosistem Go — melalui beberapa pendekatan: driver langsung pgx, database/sql, ORM GORM, dan code generator sqlc.

Setiap pendekatan punya trade-off. Episode ini memberikan peta pemilihan yang jelas, lalu membangun satu CRUD utuh yang memakai repository pattern dari episode 8.

Memilih Driver dan ORM

Empat Jalur Utama

  • pgx: driver PostgreSQL yang cepat dan modern, mendukung query SQL langsung dengan tipe data yang kaya.
  • database/sql: standard library, abstrak dan netral terhadap database apa pun.
  • GORM: ORM penuh fitur dengan auto-migration, hooks, dan query builder.
  • sqlc: men-generate kode Go dari file SQL — keamanan tipe tanpa runtime magic.
Install dependency database
go get github.com/jackc/pgx/v5
go get gorm.io/gorm gorm.io/driver/postgres

go get github.com/jackc/pgx/v5 mengambil driver pgx; baris kedua mengambil GORM beserta driver PostgreSQL-nya.

Kapan Memakai yang Mana

Pilih pgx atau database/sql untuk kontrol penuh atas SQL dan performa maksimal. Pilih GORM untuk produktivitas tinggi dengan skema sederhana. Pilih sqlc untuk query kompleks yang ingin dijamin aman tipe dan tervalidasi saat build.

pgx dan Connection Pooling

Pool Koneksi dengan pgxpool

Jangan membuat koneksi baru per request — selalu pakai pool:

Pool koneksi pgx
ctx := context.Background()
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil {
    log.Fatal(err)
}
defer pool.Close()
 
pool.Config().MaxConns = 20
pool.Config().MaxConnIdleTime = 5 * time.Minute

pgxpool.New(ctx, os.Getenv("DATABASE_URL")) membuat pool koneksi dari URL postgres://user:pass@host:5432/db. Pool dibagi antar goroutine — ribuan request bisa memakai koneksi terbatas dengan aman.

Migrasi Skema

Migrate dengan golang-migrate

Kelola perubahan skema sebagai file migrasi:

Buat dan jalankan migrasi
migrate create -ext sql -dir migrations create_users
migrate -path migrations -database "$DATABASE_URL" up

migrate create -ext sql -dir migrations create_users membuat file up dan down kosong, lalu migrate ... up menerapkan migrasi yang belum jalan. Simpan semua file migrasi di git agar riwayat skema terdokumentasi.

File up berisi skema seperti CREATE TABLE users dengan kolom id BIGSERIAL PRIMARY KEY, name, dan email UNIQUE. Kolom id memakai BIGSERIAL sehingga database mengisi otomatis.

Repository Pattern

Abstraksi Query

Repository mengemas akses data di satu tempat:

Repository pattern
type UserRepo struct {
    pool *pgxpool.Pool
}
 
func (repo *UserRepo) FindByID(ctx context.Context, id int) (User, error) {
    var u User
    err := repo.pool.QueryRow(ctx,
        "SELECT id, name, email FROM users WHERE id = $1", id).
        Scan(&u.ID, &u.Name, &u.Email)
    return u, err
}

repo.pool.QueryRow(ctx, sql, id).Scan(...) menjalankan query parameterized — placeholder $1 mencegah SQL injection, bukan string concatenation.

Insert dengan Return

Ambil nilai balik langsung dari database:

Insert mengembalikan id
func (repo *UserRepo) Create(ctx context.Context, u User) (User, error) {
    err := repo.pool.QueryRow(ctx,
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
        u.Name, u.Email).Scan(&u.ID)
    return u, err
}

RETURNING id membuat INSERT mengembalikan nilai kolom yang di-generate — satu round-trip, tanpa query tambahan.

CRUD di Dalam Handler

Merangkai Semua Lapisan

Gabungkan repository, service, dan handler menjadi satu alur:

CRUD handler lengkap
type UserHandler struct {
    repo *repository.UserRepo
}
 
func (h *UserHandler) List(w http.ResponseWriter, req *http.Request) {
    users, err := h.repo.List(req.Context())
    if err != nil {
        http.Error(w, "gagal memuat users",
            http.StatusInternalServerError)
        return
    }
    writeJSON(w, http.StatusOK, users)
}
 
func (h *UserHandler) Create(w http.ResponseWriter, req *http.Request) {
    var u User
    if err := json.NewDecoder(req.Body).Decode(&u); err != nil {
        http.Error(w, "body tidak valid", http.StatusBadRequest)
        return
    }
    created, err := h.repo.Create(req.Context(), u)
    if err != nil {
        http.Error(w, "gagal membuat user",
            http.StatusInternalServerError)
        return
    }
    writeJSON(w, http.StatusCreated, created)
}

h.repo.List(req.Context()) meneruskan context request sampai ke query database — saat klien membatalkan request, query ikut dibatalkan. Ini pola yang akan kita perdalam di episode 12.

Registrasi Router

Wire CRUD ke router
h := &handler.UserHandler{repo: userRepo}
 
r.Route("/users", func(users chi.Router) {
    users.Get("/", h.List)
    users.Post("/", h.Create)
    users.Get("/{id}", h.GetByID)
    users.Put("/{id}", h.Update)
    users.Delete("/{id}", h.Delete)
})

users.Delete("/{id}", h.Delete) menutup siklus CRUD pada satu subrouter yang rapi — persis pola subrouter yang kita bangun di episode 5.

Penutup

Inti yang harus dibawa pulang:

  • Pilih pgx, database/sql, GORM, atau sqlc sesuai kebutuhan query.
  • Selalu pakai connection pooling, bukan koneksi per request.
  • Kelola skema dengan migrasi versi seperti golang-migrate.
  • Repository mengemas query; handler tidak menulis SQL.
  • Query parameterized dengan placeholder mencegah SQL injection.
  • Context request diteruskan hingga ke database untuk pembatalan.

Di episode 10 selanjutnya kita membahas konfigurasi: configuration dan environment — variabel lingkungan dengan os.Getenv, godotenv dan Viper, config struct yang diinjeksi ke handler, serta graceful shutdown dengan signal.NotifyContext.