Database Versioning untuk Dev Teams: Panduan Praktis

by Marcus Chen
Database Versioning untuk Dev Teams: Panduan Praktis

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_verified di tabel users. Developer B tambah kolom verified_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.sql di 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.