← Semua pembelajaran / Laravel Nol → Enterprise
Fase 2 · Database & Eloquent

Migrasi — skema sebagai kode

Migrasi membuat skema database jadi bagian dari kode yang ter-review. Yang membedakan proyek kecil dan enterprise adalah apa yang boleh kamu lakukan pada tabel yang sedang dipakai.

Sumber asli laravel.com Resmi Rangkuman ~7 menit baca

Intisari

  • Satu migrasi = satu perubahan, bernomor waktu, dijalankan sekali dan tercatat di tabel migrations.
  • down() harus benar-benar membalik up() — tapi di produksi, rollback bukan strategi pemulihan.
  • Migrasi tidak boleh memakai model Eloquent: model berubah seiring waktu, migrasi lama tidak.
  • Di tabel besar, ALTER TABLE bisa mengunci tulis. Rencanakan, jangan dijalankan di jam sibuk.
  • Perubahan yang memutus kompatibilitas harus dipecah jadi beberapa deploy — pola expand & contract.

Bentuk sebuah migrasi

php artisan make:migration buat_tabel_produk
php artisan make:migration tambah_stok_ke_produk --table=produk
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('produk', function (Blueprint $table) {
            $table->id();
            $table->foreignId('kategori_id')->constrained()->cascadeOnDelete();
            $table->string('nama', 120);
            $table->string('slug')->unique();          // unique index, bukan cuma validasi
            $table->unsignedBigInteger('harga');       // rupiah dalam satuan terkecil
            $table->unsignedInteger('stok')->default(0);
            $table->json('atribut')->nullable();
            $table->timestamps();                      // created_at, updated_at
            $table->softDeletes();                     // deleted_at

            $table->index(['kategori_id', 'created_at']);   // sesuai query yang nyata
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('produk');
    }
};

Simpan uang sebagai integer. float dan double tidak bisa merepresentasikan pecahan desimal secara persis, dan kesalahannya menumpuk saat dijumlahkan. Pakai unsignedBigInteger dalam satuan terkecil (rupiah, atau sen untuk mata uang berdesimal), atau decimal(15, 2). Yang tidak boleh: float.

Perintah yang perlu dibedakan

PerintahEfekBoleh di produksi?
migrateJalankan yang belum pernah dijalankanYa (dengan --force)
migrate --pretendCetak SQL-nya saja, tidak mengeksekusiYa — biasakan sebelum deploy besar
migrate:statusMana yang sudah dan belum jalanYa
migrate:rollbackBatalkan batch terakhirHati-hati — down() jarang teruji
migrate:freshDrop semua tabel lalu migrasi ulangTIDAK PERNAH
schema:dump --prunePadatkan migrasi lama jadi satu berkas skemaYa, saat migrasi sudah ratusan

Rollback bukan rencana pemulihan produksi. down() yang menghapus kolom akan menghapus datanya juga — tidak bisa dibatalkan. Rencana pemulihan yang benar adalah maju: tulis migrasi baru yang memperbaiki, ditambah backup yang memang pernah kamu uji pemulihannya.

Jangan pakai model di dalam migrasi

// SALAH — model bisa berubah; migrasi ini akan rusak enam bulan lagi
public function up(): void
{
    Produk::whereNull('slug')->each(fn ($p) => $p->update(['slug' => Str::slug($p->nama)]));
}

// BENAR — query builder, terikat pada skema saat itu
public function up(): void
{
    DB::table('produk')->whereNull('slug')->orderBy('id')->chunkById(1000, function ($baris) {
        foreach ($baris as $b) {
            DB::table('produk')->where('id', $b->id)->update(['slug' => Str::slug($b->nama)]);
        }
    });
}

Alasannya: migrasi adalah catatan sejarah yang harus tetap bisa dijalankan dari nol bertahun-tahun kemudian. Model Produk hari ini mungkin punya global scope, cast, atau event yang belum ada saat migrasi itu ditulis — dan salah satunya bisa membuat migrasi gagal atau, lebih buruk, menulis data yang salah.

Perubahan yang memutus: expand & contract

Saat deploy berjalan, ada momen ketika kode versi lama dan versi baru berjalan bersamaan — di ECS itu justru kondisi normal selama rolling update. Karena itu, mengganti nama kolom dalam satu langkah pasti menimbulkan error. Pecah jadi tiga deploy:

DeployMigrasiKode
1 — ExpandTambah kolom nama_lengkap, nullableTulis ke kolom lama dan baru; baca yang lama
2 — MigrateIsi kolom baru untuk baris lama (per potongan)Baca yang baru, tetap tulis ke keduanya
3 — ContractHapus kolom lamaHanya pakai yang baru

Aturan turunannya, yang berlaku untuk setiap migrasi di aplikasi yang sedang hidup:

  1. Kolom baru selalu nullable atau punya default — kode lama tidak tahu cara mengisinya.
  2. Jangan pernah mengganti nama atau menghapus kolom dalam deploy yang sama dengan perubahan kodenya.
  3. Backfill data dijalankan per potongan, bukan satu UPDATE raksasa yang mengunci tabel.
  4. Menambah index di tabel besar butuh perhatian khusus — periksa dukungan operasi online di mesin databasemu.

Ini bagian yang membedakan aplikasi enterprise. Di proyek kecil, migrate berjalan saat tidak ada yang memakai aplikasi. Di aplikasi yang tidak boleh mati, setiap migrasi adalah operasi pada sistem yang sedang berjalan. Fase 9 menyambung ini ke urutan deploy ECS: migrasi dijalankan sebagai task terpisah sebelum task baru menerima lalu lintas.

Index: satu kalimat yang menghemat berjam-jam

Buat index untuk kolom yang muncul di where, join, dan order by — dan untuk kombinasinya, urutan kolom di index harus mengikuti urutan pemakaian. Ini dibahas lebih dalam di Fase 7, tapi kebiasaannya dimulai di sini: setiap kali menulis foreignId(), tanyakan apakah kamu juga akan menyaring berdasarkan kolom lain bersamanya.

Latihan: buat migrasi tabel produk seperti di atas, jalankan php artisan migrate --pretend dan baca SQL yang dihasilkan. Lalu buat migrasi kedua yang mengganti nama nama jadi judul — dan tulis rencana tiga deploy-nya sebagai komentar sebelum kamu menjalankannya.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.