BAB 17: Migrations

Mengelola struktur database secara version-controlled menggunakan sistem migrasi Laravel.

Di bab sebelumnya kita berhasil menghubungkan Laravel ke database dan melihat bagaimana raw query bekerja langsung melalui DB facade. Tapi ada satu masalah yang belum terpecahkan: setiap kali pindah mesin, berganti environment, atau bekerja dengan anggota tim baru, struktur tabelnya harus dibuat ulang secara manual. Tidak ada cara yang andal untuk memastikan semua orang punya skema yang identik.

Migrasi adalah solusinya. Alih-alih mengelola struktur database dengan SQL mentah yang disimpan di suatu tempat dan mudah terlupakan, migrasi merepresentasikan setiap perubahan skema sebagai file PHP yang masuk ke version control bersama kode aplikasi. Siapa pun yang clone repository kamu bisa menjalankan satu perintah dan mendapat database yang persis sama.

Anatomi File Migrasi

Jalankan perintah berikut untuk membuat migrasi baru:

php artisan make:migration buat_tabel_catatan

Laravel menghasilkan file di database/migrations/ dengan nama yang diawali timestamp — misalnya 2026_03_17_080000_buat_tabel_catatan.php. Timestamp inilah yang menentukan urutan eksekusi.

Isi file yang baru dibuat:

<?php
// database/migrations/2026_03_17_080000_buat_tabel_catatan.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        // Definisi perubahan skema ke depan
    }

    public function down(): void
    {
        // Cara membatalkan perubahan di up()
    }
};

Dua method ini adalah inti dari setiap migrasi. up() berisi instruksi untuk menerapkan perubahan; down() adalah kebalikannya — digunakan saat rollback. Keduanya harus selalu berpasangan dan simetris.

Membuat Tabel

Method Schema::create() menerima nama tabel dan closure yang menerima objek Blueprint. Semua kolom dan constraint didefinisikan di dalam closure ini:

<?php
// database/migrations/2026_03_17_080000_buat_tabel_catatan.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('catatan', function (Blueprint $table) {
            $table->id();                          // bigint unsigned, auto-increment, primary key
            $table->foreignId('user_id')           // bigint unsigned untuk foreign key
                  ->constrained()                  // references id on users
                  ->cascadeOnDelete();             // hapus catatan saat user dihapus
            $table->string('judul');               // varchar(255)
            $table->text('isi')->nullable();       // text, boleh null
            $table->string('prioritas', 10)
                  ->default('normal');             // varchar(10) dengan default
            $table->boolean('selesai')->default(false);
            $table->timestamps();                  // created_at + updated_at
        });
    }

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

$table->id() adalah shorthand untuk bigIncrements('id') — kolom primary key yang paling sering dipakai. timestamps() menambahkan dua kolom sekaligus: created_at dan updated_at, yang diisi otomatis oleh Eloquent.

Tipe-tipe Kolom yang Sering Dipakai

Blueprint menyediakan puluhan tipe kolom. Berikut yang paling sering muncul dalam pengembangan aplikasi web:

MethodTipe DatabaseKeterangan
id()BIGINT UNSIGNEDPrimary key auto-increment
string('nama')VARCHAR(255)Teks pendek
text('konten')TEXTTeks panjang
integer('jumlah')INTBilangan bulat
bigInteger('views')BIGINTBilangan bulat besar
decimal('harga', 10, 2)DECIMAL(10,2)Angka desimal presisi
boolean('aktif')TINYINT(1)True/false
date('tanggal_lahir')DATETanggal saja
timestamp('dikirim_pada')TIMESTAMPTanggal dan waktu
timestamps()2x TIMESTAMPcreated_at + updated_at
softDeletes()TIMESTAMPdeleted_at untuk soft delete
json('metadata')JSONData JSON
enum('status', [...])ENUMNilai dari daftar terbatas
foreignId('user_id')BIGINT UNSIGNEDKolom foreign key

Modifier Kolom

Setiap kolom bisa dimodifikasi dengan method yang dirantai:

// database/migrations/..._buat_tabel_profil.php

Schema::create('profil', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();

    $table->string('nama_lengkap');
    $table->string('telepon', 20)->nullable();          // boleh null
    $table->string('website')->nullable()->default(''); // null atau string kosong
    $table->text('bio')->nullable();
    $table->date('tanggal_lahir')->nullable();
    $table->string('kota', 100)->nullable()->after('bio'); // letakkan setelah kolom bio

    $table->timestamps();
});

Modifier yang paling sering digunakan:

  • nullable() — kolom boleh menyimpan NULL
  • default($nilai) — nilai default jika tidak diisi
  • after('kolom') — posisi kolom relatif terhadap kolom lain (MySQL)
  • comment('keterangan') — komentar pada kolom di skema

Mengubah Tabel yang Sudah Ada

Tidak semua migrasi membuat tabel baru. Untuk mengubah tabel yang sudah ada, gunakan Schema::table() bukan Schema::create():

php artisan make:migration tambah_kolom_kategori_ke_catatan
<?php
// database/migrations/2026_03_17_090000_tambah_kolom_kategori_ke_catatan.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('catatan', function (Blueprint $table) {
            $table->string('kategori', 50)
                  ->nullable()
                  ->after('judul');    // letakkan setelah kolom judul
        });
    }

    public function down(): void
    {
        Schema::table('catatan', function (Blueprint $table) {
            $table->dropColumn('kategori');
        });
    }
};

Mengubah Tipe atau Sifat Kolom

Untuk mengubah kolom yang sudah ada, tambahkan ->change() di akhir definisi:

Schema::table('catatan', function (Blueprint $table) {
    // Perbesar panjang judul dari 255 menjadi 500
    $table->string('judul', 500)->change();

    // Ubah prioritas agar boleh null
    $table->string('prioritas', 10)->nullable()->default('normal')->change();
});

->change() hanya bisa mengubah tipe dan modifier — bukan mengganti nama kolom. Untuk mengganti nama, gunakan $table->renameColumn('nama_lama', 'nama_baru') secara terpisah.

Foreign Key dan Constraint

Hubungan antar tabel didefinisikan melalui foreign key constraint. Laravel menyediakan cara ringkas menggunakan foreignId()->constrained():

// Cara ringkas — Laravel otomatis menyimpulkan nama tabel dan kolom
$table->foreignId('user_id')->constrained();

// Cara eksplisit jika nama kolom tidak mengikuti konvensi
$table->foreignId('penulis_id')
      ->constrained(table: 'users', indexName: 'catatan_penulis_id');

// Dengan perilaku cascade
$table->foreignId('kategori_id')
      ->nullable()
      ->constrained()
      ->nullOnDelete()     // set null saat kategori dihapus
      ->cascadeOnUpdate(); // update ikut saat id kategori berubah

Perilaku onDelete yang tersedia: cascadeOnDelete(), restrictOnDelete(), nullOnDelete(), noActionOnDelete(). Pemilihan yang tepat bergantung pada aturan bisnis aplikasi.

Urutan migrasi penting untuk foreign key. Tabel users harus sudah ada sebelum tabel catatan yang mereferensikannya dibuat. Timestamp di nama file migrasi menentukan urutan ini — pastikan migrasi dependen memiliki timestamp yang lebih belakang.

Indeks

Indeks mempercepat query pencarian pada kolom tertentu. Tambahkan indeks saat membuat atau mengubah tabel:

Schema::create('catatan', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('judul');
    $table->string('prioritas', 10)->default('normal');
    $table->boolean('selesai')->default(false)->index(); // indeks tunggal inline
    $table->timestamps();

    // Indeks komposit — berguna untuk query yang sering memfilter dua kolom sekaligus
    $table->index(['user_id', 'selesai']);

    // Unique constraint — mencegah duplikasi
    $table->unique(['user_id', 'judul']);
});

Untuk menghapus indeks saat rollback:

$table->dropIndex(['selesai']);            // drop indeks tunggal
$table->dropUnique(['user_id', 'judul']); // drop unique constraint
$table->dropForeign(['user_id']);          // drop foreign key constraint

Menjalankan Migrasi

Dengan file migrasi sudah dibuat, saatnya menjalankannya:

# Jalankan semua migrasi yang belum dieksekusi
php artisan migrate

# Lihat status setiap migrasi
php artisan migrate:status

# Preview SQL yang akan dieksekusi tanpa menjalankannya
php artisan migrate --pretend

Output migrate:status memperlihatkan setiap file migrasi, batch-nya, dan apakah sudah dijalankan atau belum — berguna untuk debugging ketika skema database tidak sesuai ekspektasi.

Rollback dan Reset

Ketika ada yang salah, migrasi bisa dibalik:

# Batalkan batch migrasi terakhir
php artisan migrate:rollback

# Batalkan 3 migrasi terakhir
php artisan migrate:rollback --step=3

# Batalkan semua migrasi (kembali ke database kosong)
php artisan migrate:reset

Rollback memanggil method down() dari setiap migrasi dalam urutan terbalik. Inilah alasan down() harus selalu ditulis dengan benar — rollback yang tidak berfungsi membuat debugging jadi lebih sulit.

Refresh dan Fresh

Dua perintah ini sangat berguna selama development:

# Rollback semua lalu migrate ulang dari awal
php artisan migrate:refresh

# Hapus semua tabel lalu migrate (lebih cepat dari refresh)
php artisan migrate:fresh

# Fresh + isi dengan data dummy
php artisan migrate:fresh --seed

Perbedaan refresh vs fresh: refresh memanggil down() setiap migrasi secara berurutan, sementara fresh langsung menghapus semua tabel tanpa peduli migrasi. Di development, fresh lebih sering digunakan karena lebih cepat dan tidak bergantung pada down() yang sempurna.

Jangan jalankan migrate:fresh di database production. Semua data akan hilang permanen. Gunakan --force flag dengan sangat hati-hati di lingkungan production, dan selalu backup data sebelumnya.

Squashing Migrasi

Seiring waktu, jumlah file migrasi bisa bertambah menjadi puluhan bahkan ratusan. Laravel menyediakan cara untuk “meratakan” semua migrasi lama menjadi satu file SQL dump:

# Buat schema dump tanpa menghapus file migrasi lama
php artisan schema:dump

# Buat dump dan hapus file migrasi yang sudah tercakup
php artisan schema:dump --prune

Hasilnya tersimpan di database/schema/. Saat migrate:fresh dijalankan, Laravel akan mengeksekusi dump ini terlebih dahulu, baru kemudian migrasi-migrasi baru yang belum tercakup. Ini mempercepat setup environment baru secara signifikan.

Latihan

Coba kerjakan latihan berikut untuk membiasakan diri dengan alur kerja migrasi:

  1. Buat migrasi tabel tag — Buat migrasi untuk tabel tag dengan kolom id, nama (string, unique), dan timestamps. Jalankan dengan migrate dan verifikasi dengan db:table tag.

  2. Tambah kolom baru — Buat migrasi terpisah untuk menambahkan kolom warna (string, nullable, default '#6B7280') ke tabel tag. Pastikan down() menghapus kolom tersebut, lalu uji dengan rollback dan migrate ulang.

  3. Relasi many-to-many — Buat migrasi untuk tabel pivot catatan_tag dengan kolom catatan_id dan tag_id sebagai foreign key ke tabel masing-masing. Tambahkan constraint cascadeOnDelete() pada keduanya, dan buat composite primary key dari kedua kolom tersebut menggunakan $table->primary(['catatan_id', 'tag_id']).

Penutup Bab

Migrasi mengubah pengelolaan skema database dari pekerjaan manual yang rawan human error menjadi bagian dari codebase yang bisa di-review, di-rollback, dan direplikasi secara konsisten di semua environment.

Struktur tabel sudah ada. Langkah berikutnya adalah mengisinya dengan data yang bermakna — baik untuk pengujian maupun untuk kebutuhan data awal aplikasi. Di sinilah Query Builder berperan: cara membangun query database yang ekspresif dan aman tanpa menulis SQL mentah.

Referensi

  1. 1Database: Migrations — Laravel 12.x Documentation
  2. 2Database: Schema Builder — Laravel 12.x Documentation