Kamu pernah nggak, lagi enak-enak deploy ke production, tiba-tiba aplikasi crash karena schema database di server beda sama yang di local? Kolom user_role yang kamu tambah minggu lalu ternyata belum ada di production. Panik. Rollback manual. Log error di mana-mana.
Itu bukan masalah kamu sendirian. Itu masalah klasik yang muncul ketika tim (atau kamu sendiri sebagai solo dev) nggak punya sistem yang proper buat ngelola perubahan schema database.
Solusinya: database migration tools. Dan kabar baiknya, ada banyak database migration tools open source yang production-ready, gratis, dan nggak butuh vendor lock-in.
Artikel ini bahas empat tools yang gue sendiri pernah pakai, bukan sekadar list dari dokumentasi resmi.
Kenapa Migration Manual Itu Bom Waktu
Sebelum masuk ke tools, penting buat ngerti dulu kenapa cara lama — nulis SQL manual terus dijalanin langsung di server — itu berbahaya.
- Nggak ada history. Kamu nggak tahu siapa yang alter tabel kapan.
- Nggak reproducible. Setup database di mesin baru jadi mimpi buruk.
- Rollback susah. Kalau ada yang salah, kamu harus inget apa yang diubah.
- Konflik di tim. Dua orang alter kolom yang sama di branch berbeda — selamat datang di hell.
Migration tools ngatasin semua ini dengan cara yang simpel: setiap perubahan schema disimpan sebagai file yang bisa di-version control bareng kode kamu.
1. Flyway — Simpel, Berbasis SQL Murni
Flyway adalah pilihan paling straightforward kalau kamu udah familiar dengan SQL dan nggak mau belajar DSL baru. Kamu nulis file SQL biasa, Flyway yang urus urutannya.
Install via Docker (cara paling gampang):
docker pull flyway/flyway
Struktur folder migration:
db/
migrations/
V1__create_users_table.sql
V2__add_user_role_column.sql
V3__create_posts_table.sql
Naming convention-nya ketat: V{versi}__{deskripsi}.sql. Dua underscore, bukan satu.
Isi file V2__add_user_role_column.sql:
ALTER TABLE users
ADD COLUMN user_role VARCHAR(50) NOT NULL DEFAULT 'viewer';
Jalanin migration:
docker run --rm \
-v $(pwd)/db/migrations:/flyway/sql \
flyway/flyway \
-url=jdbc:postgresql://host.docker.internal:5432/mydb \
-user=postgres \
-password=secret \
migrate
Flyway nyimpen state di tabel flyway_schema_history di database kamu. Jadi dia tahu migration mana yang udah dijalanin dan mana yang belum.
Gotcha yang gue kena: Flyway versi community nggak support undo/rollback migration. Kalau kamu butuh rollback, kamu harus bikin migration baru yang nge-revert perubahan sebelumnya. Ini sebenarnya best practice juga — forward-only migration lebih predictable.
2. Liquibase — Lebih Fleksibel, Support YAML/JSON/XML
Liquibase mirip Flyway tapi lebih powerful. Kamu bisa nulis migration dalam format SQL, YAML, JSON, atau XML. Ini berguna kalau kamu mau migration yang lebih readable atau database-agnostic.
Install:
# macOS
brew install liquibase
# atau download binary langsung
wget https://github.com/liquibase/liquibase/releases/download/v4.26.0/liquibase-4.26.0.tar.gz
Contoh changelog dalam YAML (db/changelog/db.changelog-master.yaml):
databaseChangeLog:
- changeSet:
id: 1
author: marcus
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
- column:
name: email
type: VARCHAR(255)
constraints:
nullable: false
unique: true
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
- changeSet:
id: 2
author: marcus
changes:
- addColumn:
tableName: users
columns:
- column:
name: user_role
type: VARCHAR(50)
defaultValue: viewer
constraints:
nullable: false
Jalanin migration:
liquibase \
--url=jdbc:postgresql://localhost:5432/mydb \
--username=postgres \
--password=secret \
--changeLogFile=db/changelog/db.changelog-master.yaml \
update
Cek status:
liquibase status --verbose
Output-nya bakal kasih tahu changeset mana yang belum diapply.
Gotcha: Liquibase butuh Java runtime. Kalau kamu nggak mau install Java, pakai Docker image-nya atau pakai Flyway yang lebih lightweight.
3. golang-migrate — Untuk Stack Go atau CLI Lover
Kalau kamu pakai Go, atau kamu cuma mau CLI tool yang ringan tanpa dependency Java, golang-migrate adalah jawabannya. Ini pure Go binary, single file, langsung jalan.
Install:
# macOS/Linux
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.17.0/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/local/bin/
# atau via Go
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
Buat file migration baru:
migrate create -ext sql -dir db/migrations -seq create_users_table
Perintah ini otomatis bikin dua file:
000001_create_users_table.up.sql— untuk apply migration000001_create_users_table.down.sql— untuk rollback
Isi 000001_create_users_table.up.sql:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
user_role VARCHAR(50) NOT NULL DEFAULT 'viewer',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Isi 000001_create_users_table.down.sql:
DROP TABLE IF EXISTS users;
Jalanin migration:
# Apply semua migration
migrate -path db/migrations -database "postgres://postgres:secret@localhost:5432/mydb?sslmode=disable" up
# Rollback 1 step
migrate -path db/migrations -database "postgres://postgres:secret@localhost:5432/mydb?sslmode=disable" down 1
Integrasi di kode Go kamu:
package main
import (
"log"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func runMigrations(databaseURL string) {
m, err := migrate.New(
"file://db/migrations",
databaseURL,
)
if err != nil {
log.Fatalf("Failed to initialize migrate: %v", err)
}
defer m.Close()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatalf("Migration failed: %v", err)
}
log.Println("Database migration completed")
}
Gotcha penting: Kalau migration kamu pernah gagal di tengah jalan (misalnya koneksi putus), golang-migrate bakal mark versi itu sebagai "dirty". Kamu perlu resolve manual:
migrate -path db/migrations -database "..." force 1
Angka 1 adalah versi migration terakhir yang berhasil. Ini agak annoying tapi setidaknya transparan.
4. Atlas — Modern, Schema-as-Code
Atlas adalah pendatang baru yang menarik. Pendekatan dia beda: kamu define desired state schema kamu, Atlas yang hitung diff-nya dan generate migration SQL.
Install:
curl -sSf https://atlasgo.sh | sh
Define schema di file HCL (schema.hcl):
table "users" {
schema = schema.public
column "id" {
null = false
type = bigserial
}
column "email" {
null = false
type = varchar(255)
}
column "user_role" {
null = false
type = varchar(50)
default = "viewer"
}
column "created_at" {
null = false
type = timestamp
default = sql("now()")
}
primary_key {
columns = [column.id]
}
index "users_email_key" {
unique = true
columns = [column.email]
}
}
Apply schema langsung (dev mode):
atlas schema apply \
--url "postgres://postgres:secret@localhost:5432/mydb?sslmode=disable" \
--to file://schema.hcl
Atlas bakal tunjukin diff dan minta konfirmasi sebelum execute.
Generate migration files (untuk production workflow):
atlas migrate diff add_user_role \
--dir "file://db/migrations" \
--to file://schema.hcl \
--dev-url "docker://postgres/15/dev"
Atlas butuh dev database buat generate diff yang akurat. Dia spin up container sementara kalau kamu kasih docker:// URL — ini cukup magic tapi juga berarti kamu butuh Docker.
Gotcha: HCL syntax Atlas punya learning curve. Kalau kamu prefer SQL biasa, Atlas juga support format SQL untuk schema definition-nya.
Perbandingan Singkat: Pilih yang Mana?
| Tool | Format | Rollback | Dependency | Cocok untuk |
|---|---|---|---|---|
| Flyway | SQL | Manual (migration baru) | Java/Docker | Tim yang familiar SQL |
| Liquibase | SQL/YAML/XML | Built-in (terbatas) | Java | Multi-database, enterprise |
| golang-migrate | SQL | Built-in (down files) | Go binary | Go projects, CLI lovers |
| Atlas | HCL/SQL | Diff-based | Go binary | Schema-first approach |
Kalau kamu baru mulai dan stack-nya bebas, golang-migrate adalah rekomendasi gue. Ringan, nggak butuh Java, rollback jelas dengan .down.sql files, dan dokumentasinya solid.
Kalau kamu udah di ekosistem Java atau butuh support banyak database sekaligus, Liquibase worth it.
Integrasi dengan CI/CD Pipeline
Migration tools ini baru berguna maksimal kalau diintegrasikan ke pipeline kamu. Contoh sederhana dengan GitHub Actions:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install golang-migrate
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.17.0/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/local/bin/
- name: Run database migrations
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
migrate -path db/migrations -database "$DATABASE_URL" up
Jalanin migration sebelum deploy aplikasi baru. Urutan ini penting buat backward compatibility.
Yang Gue Lakuin Sekarang
Untuk project solo gue sekarang, gue pakai golang-migrate karena stack-nya Go dan gue suka punya kontrol penuh atas SQL yang dijalanin. Setup-nya cuma butuh sekitar 15 menit dari nol sampai migration pertama jalan.
Beberapa hal yang langsung gue terapkan setelah pakai migration tools:
- Semua perubahan schema masuk Git, nggak ada lagi SQL yang diketik langsung di production terminal.
- Setiap PR yang ubah schema wajib include migration file, di-review bareng kode.
- Rollback jadi ada prosedurnya, bukan panik dan coba-coba.
Kalau kamu belum pakai database migration tools sama sekali, mulai dari project yang paling kecil dulu. Bikin satu migration file untuk schema yang udah ada (V1__initial_schema.sql), commit, dan rasain bedanya. Dari situ natural bakal ketagihan.
Database migration tools open source ini semua gratis, battle-tested, dan komunitas support-nya aktif. Nggak ada alasan buat tetap migration manual di 2024.