Belajar Echo - Integrasi Database & ORM
Episode 9 of 23

Belajar Echo - Integrasi Database & ORM

Episode ini menghubungkan Echo dengan database: koneksi PostgreSQL dengan pgx, database/sql, GORM, dan sqlc, connection pooling, migrasi skema, serta implementasi CRUD lengkap dalam handler Echo mengikuti pola repository dari episode 8.

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

Pendahuluan

Hampir semua REST API akhirnya berujung pada penyimpanan data. Episode ini menghubungkan arsitektur dari episode 8 dengan database nyata: PostgreSQL sebagai pilihan utama, disertai pendekatan dengan database/sql, GORM, dan sqlc. Kalian akan melihat pola repository benar-benar bekerja.

Episode 9 ini membahas koneksi PostgreSQL dengan pgx, pooling, migrasi skema, lalu implementasi CRUD lengkap dalam handler Echo.

Menyiapkan PostgreSQL

Menjalankan Database dengan Docker

Mulai dengan instance PostgreSQL lokal. Cara paling bersih adalah Docker:

Menjalankan PostgreSQL di Docker
docker run --name belajar-echo-pg \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_DB=belajar_echo \
  -p 5432:5432 \
  -d postgres:16

Setelah kontainer berjalan, kalian punya database belajar_echo di localhost:5432. Catat kredensialnya — akan dipakai sebagai DATABASE_URL di episode 10.

Koneksi dengan pgx dan database/sql

Pooling dengan pgxpool

pgx adalah driver PostgreSQL terpopuler untuk Go, dan pgxpool menyediakan connection pool yang sehat untuk server HTTP:

Membuat pool koneksi pgx
import (
	"context"
	"github.com/jackc/pgx/v5/pgxpool"
)
 
func NewDB(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
	pool, err := pgxpool.New(ctx, dsn)
	if err != nil {
		return nil, err
	}
	if err := pool.Ping(ctx); err != nil {
		pool.Close()
		return nil, err
	}
	return pool, nil
}

pgxpool.New membuat pool yang siap dipakai bersamaan oleh banyak goroutine — persis yang dibutuhkan server Echo yang menangani banyak request sekaligus.

Query dengan Parameter

Gunakan query berparameter untuk menghindari SQL injection:

Query berparameter dengan pgx
func (r *userRepo) FindByID(ctx context.Context, id int) (*model.User, error) {
	row := r.pool.QueryRow(ctx,
		"SELECT id, name, email FROM users WHERE id = $1", id)
	var u model.User
	if err := row.Scan(&u.ID, &u.Name, &u.Email); err != nil {
		return nil, err
	}
	return &u, nil
}

Placeholder $1 diisi dengan nilai variabel, bukan digabung string. Ini prinsip non-negotiable untuk semua query database.

Alternatif: GORM dan sqlc

GORM untuk Produktivitas

GORM adalah ORM paling populer di ekosistem Go. Cocok bila kalian ingin menghindari menulis SQL berulang:

Install GORM dan driver
go get gorm.io/gorm
go get gorm.io/driver/postgres
Koneksi dan migrasi GORM
import (
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)
 
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.AutoMigrate(&model.User{})

db.AutoMigrate membuat atau memperbarui tabel dari definisi struct. Nyaman untuk development, tapi di production migrasi sebaiknya dikelola tool khusus.

sqlc untuk SQL Tipe-Aman

sqlc menempuh jalur sebaliknya: kalian menulis SQL, dan kode Go tipe-aman dibangkitkan dari file .sql:

Install sqlc
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
sqlc init

Setiap pendekatan punya trade-off: database/sql minimal dan transparan, GORM produktif, sqlc tipe-aman. Pilih sesuai kebutuhan tim; semuanya berjalan baik di balik antarmuka repository.

Migrasi Skema

Migrasi Terkelola dengan goose

Untuk production, gunakan tool migrasi versi. goose adalah pilihan sederhana yang berbasis SQL:

Install goose
go install github.com/pressly/goose/v3/cmd/goose@latest
goose -dir migrations postgres \
  "host=localhost user=postgres password=postgres dbname=belajar_echo" up

File migrasi disimpan di direktori migrations/ dengan urutan versi. Perintah goose up menerapkan semua migrasi yang belum berjalan.

CRUD Lengkap dalam Handler Echo

Repository, Service, Handler

Dengan pola dari episode 8, CRUD dibangun tiga lapisan. Repository menyimpan query, service mengatur alur, dan handler menghubungkan ke HTTP:

CRUD lengkap dalam handler
func (h *UserHandler) Create(c echo.Context) error {
	var input model.CreateUserInput
	if err := c.Bind(&input); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	user, err := h.svc.Create(c.Request().Context(), input)
	if err != nil {
		return err
	}
	return c.JSON(http.StatusCreated, user)
}

Handler tetap tipis: bind, panggil service, kembalikan response. Semua logika transaksi dan validasi bisnis hidup di service, dan semua query di repository.

Transaksi di Level Service

Service juga yang mengelola transaksi — misalnya membuat user sekaligus mencatat audit log:

Transaksi di service
tx, err := s.db.BeginTx(ctx, nil)
defer tx.Rollback()
if _, err := tx.Exec(ctx, "INSERT INTO users ..."); err != nil {
	return err
}
if _, err := tx.Exec(ctx, "INSERT INTO audit_logs ..."); err != nil {
	return err
}
return tx.Commit()

Pola BeginTx dan Commit memastikan beberapa operasi menjadi satu kesatuan yang atomik.

Penutup

Episode 9 menghubungkan Echo dengan dunia data: pool koneksi pgx yang aman untuk concurrency, query berparameter untuk keamanan, pilihan GORM dan sqlc sesuai kebutuhan, migrasi terkelola dengan goose, serta CRUD lengkap yang tersusun dalam pola repository, service, dan handler.

Inti yang harus dibawa pulang:

  • pgxpool menyediakan koneksi aman untuk concurrency tinggi.
  • Selalu gunakan query berparameter untuk mencegah SQL injection.
  • GORM produktif untuk development; sqlc menghasilkan kode tipe-aman.
  • Migrasi production dikelola tool versi seperti goose, bukan AutoMigrate.
  • Handler tetap tipis: bind, panggil service, kembalikan response.
  • Transaksi atomik dikelola di level service.

Di episode 10 selanjutnya kita akan membahas configuration & environment — membaca variabel lingkungan dengan os.Getenv, godotenv, dan Viper, struct config terpusat, server HTTP custom dengan ReadTimeout dan WriteTimeout, serta graceful shutdown yang bersih saat server menerima sinyal berhenti.