Migration Laravel Tanpa Downtime

Setiap kali saya harus mengubah struktur tabel di database production yang sedang dipakai aktif — apalagi di sistem klinik atau rumah sakit yang jam operasionalnya nyaris 24 jam — pertanyaan pertama yang muncul selalu sama: apakah migration ini bisa jalan tanpa membuat aplikasi down, walau cuma beberapa detik? Di lingkungan kerja seperti fasilitas kesehatan, "beberapa detik down" itu bukan angka kecil kalau kebetulan bertepatan dengan input data pasien. Karena itu saya membiasakan diri untuk tidak pernah menjalankan migration yang mengubah kolom secara langsung dan sekaligus di production. Pola yang saya pakai konsisten: pisahkan perubahan skema dari perubahan data, dan pisahkan lagi dari pengetatan constraint.

Kenapa migration "sekali jalan" itu berbahaya

Migration Laravel yang menambah kolom baru dengan constraint NOT NULL langsung, pada tabel yang sudah punya banyak baris, punya dua masalah. Pertama, secara teknis database perlu mengisi nilai default ke semua baris lama sebelum constraint bisa ditegakkan — pada tabel besar ini bisa memicu table lock yang membuat query lain antre, dan di beberapa mesin database operasi ini bisa berjalan cukup lama. Kedua, dan ini yang sering terlewat: kalau aplikasi Laravel Anda dideploy dengan strategi rolling atau zero-downtime deployment, ada jeda di mana kode lama masih jalan berdampingan dengan skema baru, atau sebaliknya. Kalau kolom baru langsung NOT NULL tanpa default, kode lama yang belum tahu soal kolom itu akan gagal insert.

Pola tiga langkah: nullable, backfill, lalu ketatkan

Solusi yang saya pakai berulang kali adalah memecah satu perubahan skema jadi tiga migration/deployment terpisah, dijalankan berurutan dengan jeda waktu di antaranya (bukan sekaligus dalam satu deploy).

Langkah 1 — tambah kolom sebagai nullable. Ini operasi ringan karena tidak perlu menyentuh baris yang sudah ada.

Schema::table('patients', function (Blueprint $table) {
    $table->string('mrn_format')->nullable()->after('mrn');
});

Deploy migration ini, dan kode aplikasi Anda pada tahap ini sudah bisa mulai menulis ke kolom baru itu (opsional, boleh diisi null) tanpa perlu tahu-menahu soal baris lama.

Langkah 2 — backfill data secara terpisah, di luar siklus deploy migration. Ini bagian yang paling sering diabaikan: backfill jangan dijadikan satu migration dengan penambahan kolom. Untuk tabel besar, backfill sebaiknya jalan dalam batch kecil supaya tidak menahan lock terlalu lama dan tidak membebani database yang sedang melayani trafik live.

Patient::whereNull('mrn_format')
    ->chunkById(500, function ($patients) {
        foreach ($patients as $patient) {
            $patient->update([
                'mrn_format' => normalizeMrn($patient->mrn),
            ]);
        }
    });

Perintah semacam ini cocok dijalankan lewat Artisan command khusus, bukan di dalam file migration, supaya bisa dijalankan berulang, dipantau progresnya, dan dihentikan-lanjutkan kalau perlu — misalnya untuk tabel dengan jutaan baris, backfill semacam ini bisa memakan waktu berjam-jam, jadi menjalankannya sebagai job terjadwal atau command terpisah jauh lebih aman daripada menahannya dalam satu transaksi migration.

Langkah 3 — baru kunci dengan NOT NULL

Setelah yakin semua baris sudah terisi (cek dulu dengan query whereNull sebelum lanjut), barulah migration terakhir mengetatkan constraint:

Schema::table('patients', function (Blueprint $table) {
    $table->string('mrn_format')->nullable(false)->change();
});

Di titik ini kode aplikasi juga sudah harus dipastikan selalu mengisi kolom tersebut untuk baris baru. Urutan yang benar biasanya: deploy kode yang mengisi kolom baru → tunggu backfill selesai → baru deploy migration yang mengunci constraint. Kalau dibalik, ada risiko baris baru yang masuk di antara backfill dan constraint locking justru gagal insert.

Yang perlu diingat

Pola nullable-dulu, backfill terpisah, baru NOT NULL ini sebenarnya prinsip umum di banyak sistem database, bukan sesuatu yang khas Laravel — Laravel hanya menyediakan ->change() dan Artisan command yang memudahkan penerapannya. Aturan praktis yang saya pegang: setiap migration yang bisa mengunci tabel besar sebaiknya dianggap berisiko sampai terbukti sebaliknya, dan lebih baik tiga deployment kecil yang aman daripada satu migration besar yang harus di-rollback tengah malam.