Kode bisa diganti dalam hitungan detik dengan melempar symlink, tetapi data tidak: baris yang sudah dihapus tidak kembali hanya karena kita membatalkan rilis. Karena itu deployment yang menyentuh skema database adalah tahap paling berisiko dalam seluruh workflow. Pada chapter ini kita pelajari urutan aman antara migrasi dan kode, pola expand-contract untuk perubahan skema besar, kebiasaan cadangan sebelum deploy, serta cara memulihkan kondisi saat migrasi gagal di tengah jalan. Cara menulis migration itu sendiri — berkas, versioning, tool — sudah kita bahas pada chapter Schema Migrations ; fokus kita sekarang adalah sisi deployment-nya.
Urutan Aman Migrasi dan Kode #
Pertanyaan klasik setiap tim: jalankan migrasi sebelum atau sesudelah kode baru aktif? Jawabannya bergantung pada jenis perubahan, dan aturannya bisa dirumuskan dalam dua kalimat. Perubahan aditif — menambah kolom, tabel, atau index — harus dimigrasikan sebelum kode baru aktif, karena kode baru pasti membaca struktur yang belum ada bila urutannya dibalik. Perubahan destruktif — menghapus kolom atau tabel — hanya boleh dijalankan setelah kode lama benar-benar pensiun, karena selama kode lama masih berjalan ia masih membaca struktur yang ingin dihapus.
| Jenis perubahan | Urutan terhadap kode | Alasan |
|---|---|---|
| Tambah kolom/tabel/index | Migrasi dulu, lalu deploy kode | Kode baru butuh struktur itu |
| Ubah nilai default kolom | Migrasi dulu | Kode lama dan baru sama-sama aman |
| Isi/backfill data baru | Setelah migrasi, sebelum atau bersama kode | Bisa berjalan lambat, jangan blokir deploy |
| Hapus kolom/tabel | Setelah kode lama berhenti dipakai | Kode lama masih membacanya |
Perhatikan bahwa “deploy” di sini bukan satu momen melainkan satu periode: pada rolling deployment, dua versi kode hidup bersamaan selama beberapa menit. Struktur database harus kompatibel dengan kedua versi selama periode itu — inilah inti pola pada bagian berikutnya.
Pola Expand-Contract #
Perubahan skema yang besar — mengganti nama kolom, memecah satu tabel menjadi dua, mengubah tipe data — tidak bisa dilakukan dalam satu deploy karena versi kode lama dan baru mengharapkan struktur yang berbeda. Pola expand-contract memecahnya menjadi dua fase yang dipisahkan waktu: fase expand menambahkan struktur baru tanpa mengganggu yang lama, fase contract membersihkan struktur lama setelah tidak ada kode yang memakainya.
Pada deploy pertama kita hanya menambah — kolom baru dibuat nullable sehingga kode lama yang tidak mengetahuinya tetap aman, lalu kode baru menulis ke kolom baru sekaligus kolom lama (dual-write). Jeda antar deploy memberi waktu backfill data dan verifikasi. Deploy kedua baru menghapus struktur lama. Karena kesalahan urutan adalah bug yang halus, mari kita tulis pemeriksa sederhana untuk rangkaian langkah skema:
<?php
function periksaExpandContract(array $langkah): array
{
$faseKontrakDimulai = false;
$pelanggaran = [];
foreach ($langkah as $i => $langkahItem) {
$nomor = $i + 1;
if ($langkahItem["fase"] === "expand" && $faseKontrakDimulai) {
$pelanggaran[] = sprintf(
"Langkah %d (%s): expand setelah contract",
$nomor,
$langkahItem["nama"]
);
}
if ($langkahItem["fase"] === "contract") {
$faseKontrakDimulai = true;
}
}
return $pelanggaran;
}
$deployAman = [
["nama" => "tambah kolom nullable", "fase" => "expand"],
["nama" => "deploy kode pembaca kolom", "fase" => "expand"],
["nama" => "isi nilai kolom lama", "fase" => "expand"],
["nama" => "hapus kolom lama", "fase" => "contract"],
];
$deployBermasalah = [
["nama" => "tambah kolom nullable", "fase" => "expand"],
["nama" => "hapus kolom lama", "fase" => "contract"],
["nama" => "tambah index baru", "fase" => "expand"],
];
echo "Deploy aman: " . count(periksaExpandContract($deployAman)) . " pelanggaran\n";
foreach (periksaExpandContract($deployBermasalah) as $pesan) {
echo "Deploy bermasalah: {$pesan}\n";
}Output yang Dihasilkan:
Deploy aman: 0 pelanggaran
Deploy bermasalah: Langkah 3 (tambah index baru): expand setelah contractFungsi periksaExpandContract() menelusuri daftar langkah dan mengibarkan bendera begitu langkah contract pertama ditemui. Langkah expand apa pun setelah bendera itu naik dicatat sebagai pelanggaran, karena berarti ada struktur ditambahkan setelah pembersihan dimulai — tanda bahwa pemecahan dua fase tidak diikuti secara konsisten. Rangkaian pertama menghasilkan nol pelanggaran; rangkaian kedua menunjukkan pelanggaran pada langkah ketiga. Pemeriksa seperti ini layak menjadi bagian dari code review otomatis untuk berkas migration Anda.
Migrasi Kompatibel ke Belakang #
Kebiasaan-kebiasaan berikut menjaga setiap migration aman dijalankan bersama dua versi kode sekaligus:
- Kolom baru selalu nullable atau memiliki default — insert dari kode lama yang tidak menyebut kolom baru tetap lolos.
- Jangan rename kolom dalam satu langkah — gunakan tiga langkah: tambah kolom baru, dual-write dua kolom, hapus kolom lama setelah kode lama pensiun.
- Hindari mengubah tipe kolom secara in-place pada tabel besar; salin ke kolom baru dengan pola expand-contract.
- Jangan menghapus kolom yang masih dibaca kode yang berjalan, termasuk versi lama pada rolling deployment.
- Constraint baru diberlakukan bertahap — tambahkan sebagai nullable dulu, isi datanya, baru ketatkan NOT NULL.
Cadangan Database Sebelum Deploy #
Aturan backup-first sederhana: tidak ada migrasi yang menyentuh produksi sebelum cadangan terbaru tersedia dan segar. “Segar” berarti umurnya dibatasi — cadangan berumur tiga hari akan kehilangan data tiga hari bila restore dipaksa. Pemeriksa berikut memvalidasi daftar cadangan terhadap waktu deploy dengan batas umur maksimum:
<?php
function cadanganMasihSegar(
array $daftarCadangan,
string $waktuDeploy,
int $umurMaksJam = 24
): bool {
if ($daftarCadangan === []) {
return false;
}
sort($daftarCadangan);
$zona = new DateTimeZone("Asia/Jakarta");
$terbaru = new DateTimeImmutable(end($daftarCadangan), $zona);
$batas = (new DateTimeImmutable($waktuDeploy, $zona))
->modify("-{$umurMaksJam} hours");
return $terbaru >= $batas;
}
$waktuDeploy = "2026-01-15 12:00:00";
$segar = [
"2026-01-14 20:00:00",
"2026-01-15 03:00:00",
];
$basi = [
"2026-01-10 02:00:00",
"2026-01-11 02:00:00",
];
var_dump(cadanganMasihSegar($segar, $waktuDeploy));
var_dump(cadanganMasihSegar($basi, $waktuDeploy));
var_dump(cadanganMasihSegar([], $waktuDeploy));Output yang Dihasilkan:
bool(true)
bool(false)
bool(false)Karena timestamp format ISO disusun berurutan secara leksikal, sort() lalu end() cukup untuk menemukan cadangan terbaru tanpa membandingkan objek tanggal satu per satu. Cadangan terbaru pada skenario pertama berumur sembilan jam sehingga lolos ambang 24 jam; skenario kedua gagal karena cadangan terbaru sudah berumur empat hari; daftar kosong jelas menolak. Pada workflow nyata, fungsi seperti ini menjadi gerbang di skrip deploy: keluar dengan gagal bila false, jangan lanjut ke migrasi.
Menjalankan Migrasi di Dalam Workflow #
Migrasi produksi sebaiknya dijalankan oleh satu pelari tunggal: tahap release workflow, bukan oleh setiap server secara paralel. Dua worker yang menjalankan migrate bersamaan bisa saling menabrak atau mengunci. Framework menyediakan perintah CLI yang dirancang untuk konteks ini — berikut bentuk umumnya sebagai blok display:
php artisan migrate --force
vendor/bin/doctrine migrations:migrate --no-interactionBaris pertama adalah Laravel, baris kedua Doctrine Migrations. Flag --force dan --no-interaction sama-sama melewati konfirmasi interaktif — penting karena perintah berjalan di pipeline CI tanpa terminal yang bisa menjawab pertanyaan. Kedua perintah ini membaca tabel versi migration, membandingkannya dengan berkas migration pada rilis baru, lalu menerapkan yang belum dijalankan secara berurutan. Exit code bukan nol menghentikan skrip deploy — inilah mengapa set -e pada skrip deploy dari chapter sebelumnya penting: migrasi gagal berarti rilis tidak boleh aktif. Setelah migrasi sukses, rilis baru diaktifkan dengan pergantian symlink seperti biasa.
Pemulihan saat Migrasi Gagal #
Kegagalan migrasi punya dua wajah: gagal bersih dan gagal setengah jalan. Gagal bersih terjadi bila seluruh migrasi berjalan dalam satu transaksi yang di-rollback — PostgreSQL mendukung transaksional DDL, sehingga skema kembali seperti semula otomatis. MySQL berbeda: sebagian besar pernyataan DDL melakukan implicit commit, sehingga migrasi multi-langkah bisa berhenti setelah langkah kedua dari lima.
Untuk kasus setengah jalan, tabel versi migration menjadi peta kondisi: tool tahu langkah mana yang terakhir sukses dan mana yang belum. Dua strategi pemulihan yang lazim:
- Fix-forward — tulis migration perbaikan yang melanjutkan atau memperbaiki langkah yang gagal. Lebih aman karena tidak menyentuh data yang sudah berubah sejak kegagalan.
- Restore cadangan — kembalikan database ke titik cadangan, dengan konsekuensi kehilangan semua data sejak cadangan dibuat. Hanya untuk kerusakan parah, dan selalu didahului upaya fix-forward.
Strategi yang dipilih harus dicatat dalam runbook sebelum migrasi berisiko dijalankan, bukan disusun sambil panik saat insiden.
Catatan Kunci DDL di MySQL #
ALTER TABLE pada MySQL tidak gratis: operasi tertentu mengunci penulisan ke tabel, dan pada tabel besar prosesnya bisa berlangsung menit hingga jam. InnoDB mendukung online DDL dengan ALGORITHM=INPLACE atau ALGORITHM=INSTANT untuk banyak operasi — misalnya menambah index tanpa memblokir insert — tetapi sebagian operasi seperti mengubah tipe kolom tetap memerlukan rebuild tabel penuh.
Konsekuensi praktisnya untuk deployment: jadwalkan DDL berat di luar jam puncak, pantau durasinya di staging dengan volume data serupa, dan pecah perubahan besar menjadi beberapa migration kecil. Bila perubahan memang harus menghentikan penulisan dalam waktu lama, gunakan jendela pemeliharaan (maintenance window) yang diumumkan — lebih baik jujur mematikan fitur selama sepuluh menit daripada membuat seluruh aplikasi tampak rusak.
Ringkasan #
Deployment database berjalan aman bila migrasi dan kode diurutkan sesuai jenis perubahannya: aditif sebelum kode, destruktif setelah kode lama pensiun. Pola expand-contract memecah perubahan besar menjadi dua deploy dengan jeda verifikasi, pemeriksa cadangan memastikan jaring pengaman selalu tersedia sebelum migrasi berjalan, dan tabel versi migration menjadi titik pijak pemulihan saat migrasi gagal setengah jalan. Setelah rilis aktif dan migrasi selesai, workflow memasuki tahap verify — dan kontrak paling penting untuk tahap itu adalah endpoint health check, yang kita rancang pada chapter berikutnya.