Migration adalah file yang mencatat satu perubahan struktur database (tambah tabel, tambah kolom, hapus kolom), dijalankan berurutan, dan dicatat sudah jalan atau belum. Hasilnya, laptopmu, laptop temanmu, dan server production punya struktur yang sama persis. Pola expand/contract adalah cara mengubah struktur itu tanpa bikin aplikasi error. Bayangin renovasi dapur restoran yang tetap buka: kamu gak membongkar dapur lama sebelum dapur baru siap. Bangun dapur baru di sebelahnya (expand), pindahkan koki satu per satu, baru bongkar yang lama (contract).
🧪 Tebakan cepat: di tabel
export_productsKedai Senja, kolomprice_usd(REAL) mau digantiprice_cents(INTEGER), karena di pelajaran Tipe Data kita lihat uang sebagai REAL bisa melenceng. Satu migration langsung menambahprice_cents, mengisinya, lalu menghapusprice_usd. Kode baru baru selesai di-deploy 2 menit kemudian. Apa yang dialami pembeli selama 2 menit itu? Tebak dulu, jawabannya ada di bagian "Jalan pintas yang bikin error".
Satu file migration di Laravel
Bikin file-nya dengan artisan:
php artisan make:migration add_price_cents_to_export_products_table
Laravel membuat file bernama tanggal dan jam, misalnya 2026_10_08_090000_add_price_cents_to_export_products_table.php, lalu kamu isi:
return new class extends Migration
{
public function up(): void
{
Schema::table('export_products', function (Blueprint $table) {
$table->integer('price_cents')->nullable();
});
}
public function down(): void
{
Schema::table('export_products', function (Blueprint $table) {
$table->dropColumn('price_cents');
});
}
};
up()berisi perubahannya. Di SQLite, Laravel mengubahnya jadi SQL ini:alter table "export_products" add column "price_cents" integer.down()berisi kebalikannya, untuk membatalkan (rollback).php artisan migratemenjalankanup()semua file yang belum pernah jalan, lalu mencatat nama file-nya di tabelmigrations, lengkap dengan nomor batch.php artisan migrate:rollbackmenjalankandown()untuk batch terakhir.
File yang sudah tercatat di tabel migrations gak akan dijalankan lagi. Jadi kalau kamu mengedit migration lama yang sudah jalan di production, server itu menjawab Nothing to migrate. dan perubahanmu gak pernah sampai. Untuk perubahan berikutnya, selalu bikin migration baru.
Seberapa berisiko sebuah perubahan
| Perubahan | Risiko | Kenapa |
|---|---|---|
add column (nullable), add index |
low | Kode lama gak peduli kolom atau index baru |
rename column, change type |
medium | Kode lama masih memakai nama atau tipe lama, dan data bisa gagal dikonversi |
drop column, drop table |
high | Datanya hilang permanen, dan kode yang masih memakainya langsung error |
Perubahan additive (menambah) aman. Perubahan destructive (menghapus atau mengganti) butuh langkah yang hati-hati. Satu catatan: menambah index di tabel jutaan baris bisa mengunci tabel selama index dibuat, makanya PostgreSQL punya CREATE INDEX CONCURRENTLY.
Jalan pintas yang bikin error
Ini yang terjadi di Lab kalau DROP COLUMN jalan saat aplikasi lama masih membaca price_usd:
ALTER TABLE export_products ADD COLUMN price_cents INTEGER;
UPDATE export_products SET price_cents = CAST(ROUND(price_usd * 100) AS INTEGER);
ALTER TABLE export_products DROP COLUMN price_usd;
-- aplikasi lama membuka katalog:
SELECT name, price_usd FROM export_products;
-- no such column: price_usd
-- aplikasi lama menambah produk:
INSERT INTO export_products (name, price_usd) VALUES ('Jahe Merah', 2.75);
-- table export_products has no column named price_usd
Selama 2 menit sebelum kode baru selesai di-deploy, katalog dan halaman admin error. Itu jawaban tebakannya. Migration dan deploy kode gak pernah terjadi di detik yang sama, dan saat deploy bertahap, server lama dan baru jalan bersamaan. Jadi di setiap detik, struktur database harus cocok dengan semua versi kode yang sedang jalan.
Expand/contract, langkah demi langkah
Tabel awalnya export_products (id, name, price_usd) berisi Kopi Gayo 250 g $4.35, Teh Tarik Sachet $1.10, Gula Aren 500 g $2.20. Lima langkah ini sama persis dengan tombol di Lab:
| Langkah | Yang dijalankan | Aplikasi yang jalan |
|---|---|---|
| 1. Expand | ALTER TABLE export_products ADD COLUMN price_cents INTEGER; |
v1: baca dan tulis price_usd |
| 2. Tulis dua kolom | deploy v2 | v2: baca price_usd, tulis dua-duanya |
| 3. Backfill | UPDATE export_products SET price_cents = CAST(ROUND(price_usd * 100) AS INTEGER) WHERE price_cents IS NULL; |
v2 |
| 4. Pindah baca | deploy v3 | v3: baca dan tulis price_cents saja |
| 5. Contract | ALTER TABLE export_products DROP COLUMN price_usd; |
v3 |
- Expand menambah kolom yang nullable. Kode v1 gak tahu kolom itu ada, dan tetap jalan.
- Tulis dua kolom dulu, baru backfill. Produk yang ditambah v1 setelah backfill akan punya
price_centskosong. Dengan v2 jalan duluan, semua produk baru sudah lengkap, dan backfill cukup mengisi yang lama. - Backfill (mengisi data lama) memakai
ROUND. Tanpa itu,CAST(4.35 * 100 AS INTEGER)menghasilkan434, satu sen hilang. - Pindah baca sesudah backfill. Kalau v3 jalan duluan, produk lama tampil tanpa harga.
- Contract paling akhir, setelah gak ada kode yang menyentuh
price_usd.
Di setiap langkah, semua request di Lab berhasil. Memang lebih panjang daripada jalan pintas, tapi gak ada satu detik pun error.
📌 Di tabel besar production
- Backfill sedikit-sedikit.
UPDATEjutaan baris sekaligus bisa mengunci tabel dalam waktu lama. Kerjakan per 1.000 baris, misalnya denganchunkById()di Laravel, di background job. - Uji di staging dulu, dengan salinan data yang mirip production, dan backup sebelum langkah berisiko tinggi.
down()gak selalu bisa mengembalikan data.down()untukDROP COLUMNbisa membuat kolomnya lagi, tapi isinya sudah hilang. Untuk perubahan high, backup itulah rollback yang sebenarnya.
⚠️ Jebakan yang sering ketemu
- Mengedit migration yang sudah jalan. Tercatat di tabel
migrations, jadi gak dijalankan ulang. Bikin migration baru. - Menambah kolom
NOT NULLtanpa default. SQLite menolak:Cannot add a NOT NULL column with default value NULL. Tambahkan sebagai nullable dulu, isi, baru pasang aturannya di migration berikutnya. CASTtanpaROUNDsaat mengubah dolar ke sen.- Drop sebelum semua kode pindah. Kode lama yang masih jalan langsung error.
- Di latihan, risikonya ditulis huruf kecil:
"low","medium","high".
TL;DR: Migration adalah file perubahan struktur database yang dibuat dengan php artisan make:migration, dijalankan dengan php artisan migrate, dicatat di tabel migrations, dan gak pernah dijalankan dua kali, jadi perubahan baru selalu butuh migration baru. Additive (add column, add index) risikonya low, rename atau change type medium, drop column atau drop table high. Untuk mengganti kolom tanpa error, pakai expand/contract: tambah kolom nullable, tulis ke dua kolom, backfill, pindah baca, baru hapus kolom lama.