Kamu lagi asik develop fitur baru, terus pas git pull dan jalanin app-nya, langsung kena error:
ERROR: column "user_role" of relation "users" does not exist
LINE 1: SELECT id, email, user_role FROM users WHERE...
Klasik. Teman satu tim kamu udah tambah kolom baru di database lokal dia, tapi lupa kasih tau. Atau udah kasih tau, tapi migration file-nya nggak ikut di-commit. Atau lebih parah: dua orang bikin migration dengan nama file yang sama.
Ini bukan masalah komunikasi. Ini masalah tooling dan workflow. Dan solusinya adalah database versioning untuk dev teams yang proper.
Kenapa Schema Database Sering Jadi Sumber Konflik
Code conflict di Git masih bisa di-resolve manual. Tapi kalau schema database udah diverge antar developer, kamu bakal buang waktu berjam-jam debugging hal yang seharusnya nggak perlu di-debug.
Beberapa skenario yang paling nyebelin:
- Developer A tambah kolom
is_verifieddi tabelusers. Developer B tambah kolomverified_at. Keduanya nggak tau satu sama lain. Pas merge, dua kolom itu ada, tapi app cuma expect salah satu. - Migration file conflict: dua orang bikin file
20240115_add_user_fields.sqldi hari yang sama. Git merge-nya sukses, tapi salah satu migration nggak jalan karena nama file sama. - Schema drift di production: developer lupa jalanin migration sebelum deploy, jadi production schema beda sama staging.
Root cause-nya satu: nggak ada single source of truth untuk state schema database.
Tool yang Perlu Kamu Pilih Dulu
Sebelum bahas workflow, kamu perlu pilih migration tool yang cocok dengan stack kamu. Beberapa opsi yang battle-tested:
| Tool | Stack | Approach |
|---|---|---|
| Flyway | Java/Kotlin, tapi bisa standalone | Versioned SQL files |
| Liquibase | JVM ecosystem | XML/YAML/SQL changelog |
| golang-migrate | Go | Versioned SQL files |
| Alembic | Python/SQLAlchemy | Python scripts |
| Prisma Migrate | Node.js | Schema-first |
| dbmate | Any (standalone binary) | Versioned SQL files |
Kalau tim kamu mixed-stack atau mau sesuatu yang language-agnostic, dbmate adalah pilihan paling simpel. Satu binary, zero dependency, format SQL biasa.
Install dbmate di macOS/Linux:
# macOS
brew install dbmate
# Linux
sudo curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64
sudo chmod +x /usr/local/bin/dbmate
Setup .env di root project:
DATABASE_URL="postgres://user:password@localhost:5432/myapp_dev?sslmode=disable"
Setup Database Versioning untuk Dev Teams dengan dbmate
Struktur folder yang gue rekomendasiin:
project/
├── db/
│ ├── migrations/
│ │ ├── 20240115120000_create_users.sql
│ │ ├── 20240116090000_add_user_role.sql
│ │ └── 20240120143000_add_verified_at.sql
│ └── schema.sql # auto-generated, jangan edit manual
├── .env
└── .env.example
Buat migration baru:
dbmate new add_user_role
# Output: db/migrations/20240116090000_add_user_role.sql
Isi migration file-nya:
-- migrate:up
ALTER TABLE users ADD COLUMN user_role VARCHAR(50) NOT NULL DEFAULT 'member';
CREATE INDEX idx_users_user_role ON users(user_role);
-- migrate:down
DROP INDEX IF EXISTS idx_users_user_role;
ALTER TABLE users DROP COLUMN IF EXISTS user_role;
Jalanin semua pending migrations:
dbmate up
Cek status migration:
dbmate status
Output-nya bakal keliatan mana yang udah jalan dan mana yang belum:
[ ] 20240115120000_create_users.sql
[x] 20240116090000_add_user_role.sql
[ ] 20240120143000_add_verified_at.sql
Workflow Git yang Bikin Schema Nggak Bentrok
Tool doang nggak cukup. Kamu butuh workflow yang disepakati seluruh tim.
1. Migration File Selalu Masuk Git
Ini aturan nomor satu. Setiap kali kamu bikin migration, file-nya harus ikut di-commit bareng code yang butuh schema baru itu. Jangan pernah commit code yang depend on schema change tanpa commit migration file-nya sekalian.
git add db/migrations/20240116090000_add_user_role.sql
git add app/models/user.go # code yang pakai kolom baru
git commit -m "feat: add user_role column to users table"
2. Timestamp-based Naming, Bukan Sequential
Jangan pakai 001_, 002_, 003_. Pakai timestamp dengan format YYYYMMDDHHMMSS. Kenapa? Karena kalau dua developer bikin migration di hari berbeda, timestamp mereka pasti beda. Conflict cuma terjadi kalau dua orang bikin migration di detik yang sama — dan itu nyaris mustahil.
dbmate otomatis pakai format ini waktu kamu jalanin dbmate new.
3. Tambahkan dbmate up ke Setup Script
Buat file scripts/setup.sh atau tambahkan ke Makefile:
# Makefile
.PHONY: setup dev migrate
setup:
cp .env.example .env
dbmate up
echo "Setup done. Run 'make dev' to start."
dev:
go run ./cmd/server # atau npm run dev, python manage.py runserver, dll
migrate:
dbmate up
migrate-down:
dbmate down
migrate-status:
dbmate status
Jadi setiap developer baru join atau setiap kali pull dari main, tinggal jalanin make migrate dan schema langsung up-to-date.
4. Tambahkan Migration Check di CI/CD
Ini yang sering dilupain. Tambahkan step di pipeline CI kamu untuk verifikasi semua migration bisa jalan clean:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: myapp_test
options: >
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Install dbmate
run: |
sudo curl -fsSL -o /usr/local/bin/dbmate \
https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64
sudo chmod +x /usr/local/bin/dbmate
- name: Run migrations
env:
DATABASE_URL: postgres://postgres:testpass@localhost:5432/myapp_test?sslmode=disable
run: dbmate up
- name: Run tests
run: go test ./...
Kalau ada migration yang broken, CI langsung fail sebelum sempat merge ke main.
Gotcha yang Pernah Gue Kena
Gotcha #1: Migration Irreversible yang Nggak Dikasih Warning
Kamu drop kolom di migration up, tapi lupa isi bagian down. Pas kamu perlu rollback karena ada bug, dbmate down nggak bisa balik ke state sebelumnya.
Solusi: biasain selalu isi bagian -- migrate:down, meski cuma komentar yang jelasin kenapa rollback nggak bisa dilakukan:
-- migrate:up
ALTER TABLE orders DROP COLUMN legacy_status;
-- migrate:down
-- WARNING: Cannot restore dropped column legacy_status.
-- Data sudah hilang. Restore dari backup jika perlu.
-- ALTER TABLE orders ADD COLUMN legacy_status VARCHAR(50);
Gotcha #2: Migration Jalan di Lokal tapi Gagal di Production
Biasanya karena perbedaan versi PostgreSQL atau MySQL antara lokal dan production. Misalnya, syntax IF NOT EXISTS untuk ADD COLUMN baru support di PostgreSQL 9.6+. Kalau production masih pakai versi lama, migration bakal fail.
Solusinya: samain versi database antara lokal, staging, dan production. Pakai Docker untuk lokal development:
# docker-compose.yml
services:
db:
image: postgres:15.4-alpine # pin ke versi yang sama dengan production
environment:
POSTGRES_PASSWORD: devpass
POSTGRES_DB: myapp_dev
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Gotcha #3: Schema Drift karena Manual Query di Production
Ini yang paling bahaya. Ada developer (atau kamu sendiri, ngaku aja) yang pernah jalanin ALTER TABLE langsung di production database tanpa bikin migration file. Hasilnya: production schema beda sama yang ada di Git.
Deteksi drift dengan cara compare schema dump production vs schema.sql di repo:
# Dump schema dari production
pg_dump --schema-only $PRODUCTION_DATABASE_URL > /tmp/prod_schema.sql
# Compare dengan schema di repo
diff db/schema.sql /tmp/prod_schema.sql
Kalau ada diff, kamu perlu bikin migration yang "catch up" production ke state yang seharusnya.
Gotcha #4: Migration File yang Di-edit Setelah Jalan
Jangan pernah edit migration file yang udah pernah dijalanin di environment manapun. dbmate (dan semua migration tool lain) track migration berdasarkan checksum file. Kalau kamu edit file yang udah jalan, tool bakal detect mismatch dan bisa refuse to run atau — lebih parah — skip migration itu.
Kalau ada yang perlu difix, buat migration baru:
dbmate new fix_user_role_default_value
Langkah Lanjutan
Setelah workflow dasar ini jalan, ada beberapa hal yang bisa kamu explore:
1. Schema documentation otomatis — Tools seperti SchemaSpy atau tbls bisa generate dokumentasi schema dari database langsung. Integrate ke CI biar docs selalu up-to-date.
2. Pre-commit hook untuk validasi migration — Pasang hook yang otomatis cek apakah ada migration file baru yang belum di-format dengan benar:
# .git/hooks/pre-commit
#!/bin/bash
if git diff --cached --name-only | grep -q "db/migrations/"; then
echo "Migration files detected. Running dbmate status check..."
dbmate status
fi
3. Separate migration untuk DDL dan DML — Pisahkan migration yang ubah struktur (DDL: CREATE, ALTER, DROP) dengan migration yang ubah data (DML: INSERT, UPDATE). Ini bikin rollback lebih predictable.
4. Blue-green deployment dengan backward-compatible migrations — Kalau kamu udah di level ini, pelajari teknik expand-contract pattern: tambah kolom baru dulu (expand), migrate data, baru drop kolom lama (contract). Ini bikin zero-downtime deployment lebih aman.
Yang gue lakuin sekarang: semua project, bahkan solo project, gue setup dbmate dari hari pertama. Bukan karena over-engineering, tapi karena schema drift itu silent killer — kamu nggak tau ada masalah sampai udah terlambat. Lima menit setup di awal nghemat berjam-jam debugging di kemudian hari.
Kalau tim kamu belum punya workflow database versioning yang proper, mulai dari yang simpel dulu: commit semua migration file, pakai timestamp naming, dan tambahkan dbmate up ke setup script. Tiga hal itu aja udah eliminasi 80% konflik schema yang biasa terjadi. Untuk informasi lebih lanjut tentang optimasi database di WordPress, kamu bisa baca WordPress object cache yang membahas Redis vs Memcached untuk performa yang lebih baik.