Menghubungkan Go Fiber dengan PostgreSQL bukan hanya soal membuat koneksi lalu menjalankan query. Pada aplikasi produksi, tantangannya adalah menjaga koneksi tetap efisien, memastikan query bisa dibatalkan saat request berhenti, mengelola transaksi multi-step dengan aman, serta mempertahankan performa ketika volume data membesar. Artikel ini membahas pendekatan yang praktis untuk Go Fiber v3 dengan fokus pada connection pooling, query context-aware, transaksi, dan repository/service pattern.
Contoh yang digunakan adalah skenario umum di aplikasi bisnis: import data Excel ke PostgreSQL dalam jumlah besar, lalu melakukan validasi, upsert, dan rollback jika ditemukan masalah. Pendekatan ini relevan untuk sistem inventory, master data, pricing, HR, atau aplikasi operasional lainnya.
Mengapa connection pooling penting
PostgreSQL adalah database berbasis proses, dan membuat koneksi baru untuk setiap request sangat mahal. Karena itu, pada aplikasi Go, praktik yang benar adalah menggunakan satu objek koneksi bersama yang dikelola pool. Pada driver modern seperti pgx, pool memungkinkan aplikasi meminjam koneksi, menggunakannya, lalu mengembalikannya tanpa membuat koneksi baru setiap saat.
Dengan pooling, kita mendapatkan beberapa manfaat:
- Latency lebih stabil karena koneksi sudah tersedia.
- Beban database lebih terkendali karena jumlah koneksi dibatasi.
- Pemakaian resource aplikasi lebih efisien dibanding membuka-tutup koneksi berulang.
- Lebih aman untuk beban paralel pada API dengan banyak request bersamaan.
Kesalahan yang cukup sering terjadi adalah menyamakan pool dengan koneksi tunggal, lalu mengatur nilainya terlalu tinggi. Jumlah pool yang terlalu besar justru bisa membebani PostgreSQL, terutama jika CPU database terbatas atau query sering lambat. Nilai ideal bergantung pada beban, ukuran instance database, dan pola query, jadi jangan langsung menggunakan angka besar tanpa observasi.
Memilih library dan struktur proyek
Untuk integrasi PostgreSQL di Go saat ini, kombinasi yang umum dan solid adalah:
- Fiber v3 sebagai HTTP framework.
- pgx/v5 untuk PostgreSQL.
- pgxpool untuk connection pooling.
- context.Context untuk query yang bisa dibatalkan atau timeout.
Struktur proyek yang rapi akan membantu ketika logika bisnis mulai kompleks. Salah satu struktur yang sederhana namun cukup kuat adalah memisahkan lapisan berikut:
- handler: menerima request HTTP dan mengubahnya menjadi input untuk service.
- service: berisi aturan bisnis, validasi lintas langkah, dan alur transaksi.
- repository: berisi akses database.
- db/config: inisialisasi pool, migration, dan konfigurasi.
Dengan pola ini, kode database tidak tersebar di handler, dan transaksi tidak tercampur dengan parsing HTTP secara langsung.
Inisialisasi PostgreSQL pool di Go Fiber v3
Berikut contoh inisialisasi pool PostgreSQL menggunakan pgxpool. Contoh ini menambahkan pengaturan dasar seperti jumlah koneksi maksimum, idle timeout, dan health check period.
package db
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type Config struct {
URL string
MaxConns int32
MinConns int32
MaxConnLifetime time.Duration
MaxConnIdleTime time.Duration
HealthCheckPeriod time.Duration
}
func NewPostgresPool(ctx context.Context, cfg Config) (*pgxpool.Pool, error) {
poolCfg, err := pgxpool.ParseConfig(cfg.URL)
if err != nil {
return nil, fmt.Errorf("parse db config: %w", err)
}
poolCfg.MaxConns = cfg.MaxConns
poolCfg.MinConns = cfg.MinConns
poolCfg.MaxConnLifetime = cfg.MaxConnLifetime
poolCfg.MaxConnIdleTime = cfg.MaxConnIdleTime
poolCfg.HealthCheckPeriod = cfg.HealthCheckPeriod
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
if err != nil {
return nil, fmt.Errorf("create pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping db: %w", err)
}
return pool, nil
}Contoh penggunaan di aplikasi Fiber:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
pool, err := db.NewPostgresPool(ctx, db.Config{
URL: os.Getenv("DATABASE_URL"),
MaxConns: 20,
MinConns: 5,
MaxConnLifetime: 30 * time.Minute,
MaxConnIdleTime: 10 * time.Minute,
HealthCheckPeriod: 1 * time.Minute,
})
if err != nil {
log.Fatal(err)
}
defer pool.Close()Beberapa catatan penting:
- Ping saat startup penting agar aplikasi gagal lebih awal jika database tidak bisa diakses.
- MaxConns jangan ditentukan sembarangan. Mulailah konservatif, lalu sesuaikan berdasarkan observasi.
- Close() perlu dipanggil saat shutdown agar resource dilepas dengan baik.
Query context-aware di Fiber
Query database sebaiknya menggunakan context.Context, bukan memanggil query tanpa context. Alasannya sederhana: request HTTP bisa timeout, client bisa membatalkan koneksi, atau server bisa menerapkan batas waktu tertentu. Dengan context, query di database bisa ikut dihentikan sehingga pool tidak dipenuhi query yang sebenarnya sudah tidak dibutuhkan.
Pada handler Fiber, kita bisa membuat context dengan timeout, lalu meneruskannya ke service dan repository.
func (h *ProductHandler) List(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(c.Context(), 3*time.Second)
defer cancel()
products, err := h.service.ListProducts(ctx)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(products)
}Inti praktik baiknya adalah:
- Jangan membuat query tanpa context pada request path.
- Jangan hardcode timeout terlalu kecil tanpa memahami pola query.
- Gunakan timeout berbeda untuk operasi ringan dan batch import.
Jika Anda memakai job background, gunakan context terpisah dari request HTTP agar tidak ikut dibatalkan ketika client menutup koneksi.
Repository dan service pattern
Repository pattern memisahkan detail SQL dari logika bisnis. Service pattern mengatur orkestrasi proses, validasi, dan transaksi. Ini sangat berguna pada skenario import batch karena prosesnya jarang hanya satu query sederhana.
Contoh interface repository
package repository
import (
"context"
"github.com/jackc/pgx/v5"
)
type ProductRow struct {
SKU string
Name string
Price int64
}
type ProductRepository interface {
UpsertProducts(ctx context.Context, tx pgx.Tx, rows []ProductRow) error
ExistsDuplicateSKUInBatch(rows []ProductRow) error
}Contoh service
package service
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
"myapp/repository"
)
type ProductService struct {
pool *pgxpool.Pool
repo repository.ProductRepository
}
func (s *ProductService) ImportProducts(ctx context.Context, rows []repository.ProductRow) error {
if len(rows) == 0 {
return fmt.Errorf("data import kosong")
}
if err := s.repo.ExistsDuplicateSKUInBatch(rows); err != nil {
return err
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx)
if err := s.repo.UpsertProducts(ctx, tx, rows); err != nil {
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit tx: %w", err)
}
return nil
}Perhatikan penggunaan defer tx.Rollback(ctx). Ini adalah pola aman karena jika fungsi keluar lebih awal akibat error, transaksi tetap dibatalkan. Saat Commit() berhasil, rollback yang dipanggil setelahnya akan menjadi tidak relevan dan umumnya aman diabaikan.
Transaksi untuk operasi multi-step
Transaksi diperlukan ketika beberapa langkah harus dianggap sebagai satu unit kerja. Misalnya pada import Excel:
- Validasi struktur data.
- Periksa duplikasi SKU di file.
- Simpan data ke tabel utama.
- Simpan log import.
- Update tabel ringkasan atau audit.
Jika salah satu langkah gagal, seluruh perubahan sebaiknya dibatalkan agar data tetap konsisten. Tanpa transaksi, Anda bisa berakhir pada kondisi setengah tersimpan: sebagian produk masuk, log tidak masuk, atau audit tidak sinkron.
Gunakan transaksi untuk menjaga konsistensi, bukan untuk membungkus seluruh proses parsing file yang memakan waktu lama. Parsing Excel sebaiknya dilakukan terlebih dahulu di luar transaksi, lalu transaksi dibuka saat siap menulis ke database.
Contoh upsert batch untuk hasil import Excel
Misalkan kita memiliki tabel berikut:
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
price BIGINT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Untuk skenario import, upsert berguna agar baris dengan SKU yang sudah ada diperbarui, sedangkan SKU baru ditambahkan. Teknik ini cocok untuk sinkronisasi master data dari Excel.
func (r *productRepository) UpsertProducts(ctx context.Context, tx pgx.Tx, rows []repository.ProductRow) error {
batch := &pgx.Batch{}
query := `
INSERT INTO products (sku, name, price, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (sku)
DO UPDATE SET
name = EXCLUDED.name,
price = EXCLUDED.price,
updated_at = NOW()`
for _, row := range rows {
batch.Queue(query, row.SKU, row.Name, row.Price)
}
br := tx.SendBatch(ctx, batch)
defer br.Close()
for range rows {
if _, err := br.Exec(); err != nil {
return err
}
}
return nil
}Pendekatan batch seperti ini mengurangi overhead round-trip dibanding memanggil query terpisah tanpa batching. Namun ada trade-off penting:
- Batch terlalu besar bisa membuat memori naik dan memperpanjang durasi transaksi.
- Jika satu baris gagal, keseluruhan transaksi biasanya dibatalkan.
- Untuk data sangat besar, pertimbangkan pecah menjadi chunk, misalnya 500 atau 1000 baris per batch.
Rollback saat validasi gagal
Skenario umum: file Excel berisi SKU duplikat, nama kosong, atau harga negatif. Validasi dasar sebaiknya dilakukan sebelum transaksi dimulai agar database tidak dibebani proses yang pasti gagal. Tetapi validasi yang bergantung pada kondisi database tetap bisa dilakukan di dalam alur transaksi.
func validateRows(rows []repository.ProductRow) error {
seen := make(map[string]struct{})
for i, row := range rows {
if row.SKU == "" || row.Name == "" {
return fmt.Errorf("baris %d tidak valid: sku/nama kosong", i+1)
}
if row.Price < 0 {
return fmt.Errorf("baris %d tidak valid: harga negatif", i+1)
}
if _, ok := seen[row.SKU]; ok {
return fmt.Errorf("duplikasi sku di file: %s", row.SKU)
}
seen[row.SKU] = struct{}{}
}
return nil
}Jika validasi gagal di tengah proses service, cukup kembalikan error. Karena transaksi belum commit, deferred rollback akan membatalkan semua perubahan.
Strategi batching untuk volume data besar
Pada import Excel ribuan hingga ratusan ribu baris, performa akan dipengaruhi oleh beberapa faktor: ukuran batch, jumlah indeks, kompleksitas upsert, dan lama transaksi berjalan.
Prinsip batching yang aman
- Jangan kirim semua baris sekaligus jika file sangat besar.
- Gunakan chunk dengan ukuran masuk akal, misalnya 500–2000 baris, lalu ukur hasilnya.
- Jaga transaksi tetap pendek agar lock tidak menumpuk terlalu lama.
- Pisahkan parsing dan penyimpanan supaya database hanya bekerja saat data siap.
Pada beban sangat besar, ada pendekatan lain seperti staging table, lalu melakukan merge ke tabel utama. Ini sering lebih cepat dan lebih mudah diaudit dibanding upsert satu per satu, tetapi implementasinya lebih kompleks.
Tips indexing untuk upsert dan query cepat
Upsert dengan ON CONFLICT (sku) membutuhkan constraint atau unique index pada kolom konflik. Tanpa indeks yang tepat, PostgreSQL tidak bisa menyelesaikan konflik dengan efisien.
Beberapa tips indexing yang relevan:
- Buat UNIQUE INDEX atau unique constraint pada kolom kunci bisnis seperti
sku. - Tambahkan indeks hanya pada kolom yang benar-benar dipakai untuk pencarian, filter, atau join.
- Terlalu banyak indeks akan memperlambat INSERT dan UPDATE karena semua indeks ikut diperbarui.
- Jika sering query berdasarkan
updated_atatau kombinasi kolom tertentu, pertimbangkan composite index setelah menganalisis pola query.
Kesalahan umum adalah menambahkan banyak indeks sejak awal tanpa kebutuhan nyata. Untuk workload import berat, setiap indeks tambahan punya biaya tulis.
Error handling dan debugging
Pada aplikasi produksi, error database sebaiknya tidak langsung dikirim mentah ke klien. Tangkap error teknis di repository atau service, lalu ubah menjadi pesan yang aman dan jelas di handler.
Beberapa tips debugging yang praktis:
- Log durasi query lambat agar tahu bottleneck ada di database atau aplikasi.
- Catat ukuran batch saat import untuk mempermudah tuning.
- Amati statistik pool seperti koneksi aktif, idle, dan antrian jika tersedia di instrumentasi aplikasi.
- Gunakan EXPLAIN ANALYZE untuk query yang lambat, terutama pada pencarian dan upsert kompleks.
- Periksa lock dan long transaction jika throughput turun saat import paralel.
Selain itu, bedakan error validasi bisnis dengan error infrastruktur. SKU duplikat dalam file adalah error validasi; koneksi database putus adalah error infrastruktur. Keduanya sebaiknya ditangani berbeda.
Rekomendasi implementasi di proyek nyata
Jika Anda ingin solusi yang maintainable, pola berikut layak dijadikan baseline:
- Inisialisasi satu pgxpool.Pool saat startup aplikasi.
- Gunakan context dengan timeout pada setiap operasi database dari request.
- Letakkan SQL di repository, bukan di handler.
- Kelola transaksi di service ketika ada lebih dari satu langkah yang harus konsisten.
- Lakukan validasi file Excel sebelum transaksi dibuka.
- Gunakan batching dan chunking untuk import volume besar.
- Pastikan kolom conflict target pada upsert memiliki unique index yang benar.
Pendekatan ini tidak hanya membuat aplikasi lebih cepat, tetapi juga lebih mudah diuji, dipelihara, dan diobservasi saat masalah muncul di produksi.
Penutup
Integrasi PostgreSQL di Go Fiber v3 sebaiknya dirancang dengan mempertimbangkan efisiensi koneksi, pembatalan query, konsistensi data, dan performa jangka panjang. Connection pooling membantu menjaga penggunaan koneksi tetap sehat, context-aware query mencegah query yatim saat request berakhir, dan transaksi menjaga operasi multi-step tetap konsisten. Untuk kasus import Excel batch, gabungan validasi awal, upsert yang tepat, rollback otomatis, serta strategi batching dan indexing yang baik akan sangat menentukan hasil akhir.
Jika kebutuhan Anda berkembang lebih jauh, langkah berikutnya yang layak dipertimbangkan adalah menambahkan migration tool, observability untuk query lambat, worker background untuk import asynchronous, dan pengujian integrasi terhadap PostgreSQL nyata agar perilaku transaksi benar-benar tervalidasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!