โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 4 ยท Database & Persistensi

Migrasi skema

Migrasi bukan sekadar cara membuat tabel. Ia adalah kontrak antara versi lama dan versi baru aplikasimu yang selama rolling update berjalan bersamaan.

Sumber asli pkg.go.dev Resmi Rangkuman ~8 menit baca

Intisari

  • Satu perubahan = dua berkas: NNNN_nama.up.sql dan .down.sql. Versi tercatat di tabel di database.
  • Migrasi dijalankan sebagai langkah terpisah saat deploy, bukan di entrypoint container web.
  • Selama rolling update, kode lama dan baru berjalan bersamaan โ€” jadi setiap migrasi harus kompatibel dua arah.
  • Ganti nama kolom = tiga deploy, bukan satu.
  • CREATE INDEX CONCURRENTLY di Postgres agar tabel besar tidak terkunci โ€” dan ia tidak boleh di dalam transaksi.

Bentuk berkasnya

migrations/
โ”œโ”€โ”€ 000001_buat_produk.up.sql
โ”œโ”€โ”€ 000001_buat_produk.down.sql
โ”œโ”€โ”€ 000002_tambah_index_nama.up.sql
โ”œโ”€โ”€ 000002_tambah_index_nama.down.sql
โ”œโ”€โ”€ 000003_tambah_kolom_slug.up.sql
โ””โ”€โ”€ 000003_tambah_kolom_slug.down.sql
-- 000001_buat_produk.up.sql
CREATE TABLE produk (
    id           bigserial PRIMARY KEY,
    nama         text        NOT NULL,
    harga        bigint      NOT NULL CHECK (harga >= 0),
    dibuat       timestamptz NOT NULL DEFAULT now(),
    dihapus_pada timestamptz
);

-- 000001_buat_produk.down.sql
DROP TABLE produk;
migrate -path migrations -database "$DATABASE_URL" up
migrate -path migrations -database "$DATABASE_URL" down 1
migrate -path migrations -database "$DATABASE_URL" version
migrate -path migrations -database "$DATABASE_URL" force 3   # setelah gagal

Menyematkan migrasi ke dalam binari

//go:embed migrations/*.sql
var migrasiFS embed.FS

func Migrasi(dsn string) error {
	src, err := iofs.New(migrasiFS, "migrations")
	if err != nil {
		return err
	}
	m, err := migrate.NewWithSourceInstance("iofs", src, dsn)
	if err != nil {
		return err
	}
	defer m.Close()

	if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
		return fmt.Errorf("migrasi: %w", err)
	}
	return nil
}

Ini yang membuat cmd/migrate/main.go jadi satu binari mandiri yang bisa dijalankan sebagai ECS run-task saat deploy โ€” tanpa perlu menyalin berkas SQL ke mana pun. Image yang sama berisi server dan migrasinya, jadi versi skema dan versi kode tidak pernah berbeda.

Di mana migrasi dijalankan

CaraLayak?Alasan
Di entrypoint container webโŒSepuluh task menjalankannya bersamaan saat scale-out
Di init() aplikasiโŒSama, plus startup jadi lambat dan bisa gagal separuh jalan
Langkah pipeline sebelum deployโœ…Sekali, terkendali, gagalnya menghentikan deploy
ECS run-task khususโœ…Image dan jaringan yang sama dengan aplikasi
Manual oleh manusiaโŒCepat atau lambat ada yang lupa

golang-migrate mengambil advisory lock Postgres sehingga dua proses tidak menjalankan migrasi yang sama bersamaan. Itu melindungi dari kerusakan, tapi tidak menyelesaikan masalah sesungguhnya: kalau migrasi berada di entrypoint, sembilan task lain akan menunggu lock itu sebelum bisa melayani permintaan โ€” dan deploy-mu jadi jauh lebih lama daripada seharusnya.

Kompatibel dua arah

Selama rolling update:

  waktu โ†’
  v1 v1 v1 v1        โ† semua lama
  v1 v1 v1 v2        โ† BERCAMPUR: v1 dan v2 memakai SKEMA YANG SAMA
  v1 v1 v2 v2
  v2 v2 v2 v2        โ† semua baru

Karena baris tengah itu ada โ€” selama beberapa menit di setiap deploy โ€” skema harus bisa dipakai oleh kedua versi kode sekaligus.

PerubahanAman?Catatan
Tambah tabelโœ…Kode lama tidak tahu ia ada
Tambah kolom nullable / berdefaultโœ…INSERT kode lama tetap sah
Tambah kolom NOT NULL tanpa defaultโŒINSERT kode lama langsung gagal
Hapus kolomโŒSELECT kode lama gagal
Ganti nama kolomโŒButuh tiga deploy
Ganti tipe kolomโš ๏ธMelebarkan biasanya aman; menyempitkan tidak
Tambah indexโœ…Pakai CONCURRENTLY di tabel besar
Tambah constraint NOT NULLโš ๏ธIsi dulu, validasi belakangan

Ganti nama kolom dalam tiga deploy

Deploy 1  migrasi: ALTER TABLE produk ADD COLUMN judul text;
          kode   : TULIS ke nama DAN judul; BACA dari nama

Deploy 2  migrasi: UPDATE produk SET judul = nama WHERE judul IS NULL;
          kode   : TULIS ke keduanya; BACA dari judul

Deploy 3  migrasi: ALTER TABLE produk DROP COLUMN nama;
          kode   : hanya judul

Terasa berlebihan sampai kamu pernah mengalami deploy yang gagal di tengah dan harus mengembalikan versi lama โ€” dengan skema yang sudah tidak mendukungnya lagi. Ini juga alasan rollback tidak boleh mengandalkan down.sql: mengembalikan kode itu murah, mengembalikan skema yang sudah kehilangan data itu mustahil.

Migrasi pada tabel besar

-- โŒ mengunci seluruh tabel; di 50 juta baris bisa berjam-jam
CREATE INDEX idx_produk_nama ON produk (nama);

-- โœ… tidak mengunci tulisan. TIDAK BOLEH di dalam transaksi โ€”
--    beri tahu golang-migrate lewat komentar berikut.
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_produk_nama ON produk (nama);
-- 000004_index_nama.up.sql
-- migrate:no-transaction
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_produk_nama ON produk (nama);
OperasiDi tabel besar
ADD COLUMN dengan defaultCepat di Postgres 11+ (tidak menulis ulang tabel)
ALTER COLUMN TYPEMenulis ulang seluruh tabel โ€” jadwalkan, atau pakai kolom baru
SET NOT NULLMemindai seluruh tabel; pakai CHECK ... NOT VALID lalu VALIDATE
UPDATE semua barisPecah jadi batch; satu transaksi raksasa membengkakkan WAL

Setiap ALTER TABLE mengantre di belakang transaksi yang sedang berjalan โ€” dan selama menunggu, ia memblokir semua kueri baru ke tabel itu. Satu transaksi lama yang lupa di-commit bisa membuat migrasi sepele membekukan seluruh aplikasi. Pasang SET lock_timeout = '3s' di awal migrasi supaya ia menyerah dan gagal, alih-alih menahan produksi.

Latihan: tulis migrasi yang menambah kolom slug, jalankan up lalu down, dan periksa isi tabel schema_migrations di antara keduanya. Lalu tulis rencana tiga-deploy lengkap (berkas SQL + perubahan kode per langkah) untuk mengganti nama harga jadi harga_rupiah tanpa jeda.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.