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:
| Method | Tipe Database | Keterangan |
|---|---|---|
id() | BIGINT UNSIGNED | Primary key auto-increment |
string('nama') | VARCHAR(255) | Teks pendek |
text('konten') | TEXT | Teks panjang |
integer('jumlah') | INT | Bilangan bulat |
bigInteger('views') | BIGINT | Bilangan bulat besar |
decimal('harga', 10, 2) | DECIMAL(10,2) | Angka desimal presisi |
boolean('aktif') | TINYINT(1) | True/false |
date('tanggal_lahir') | DATE | Tanggal saja |
timestamp('dikirim_pada') | TIMESTAMP | Tanggal dan waktu |
timestamps() | 2x TIMESTAMP | created_at + updated_at |
softDeletes() | TIMESTAMP | deleted_at untuk soft delete |
json('metadata') | JSON | Data JSON |
enum('status', [...]) | ENUM | Nilai dari daftar terbatas |
foreignId('user_id') | BIGINT UNSIGNED | Kolom 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 NULLdefault($nilai)— nilai default jika tidak diisiafter('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:
-
Buat migrasi tabel tag — Buat migrasi untuk tabel
tagdengan kolomid,nama(string, unique), dantimestamps. Jalankan denganmigratedan verifikasi dengandb:table tag. -
Tambah kolom baru — Buat migrasi terpisah untuk menambahkan kolom
warna(string, nullable, default'#6B7280') ke tabeltag. Pastikandown()menghapus kolom tersebut, lalu uji denganrollbackdanmigrateulang. -
Relasi many-to-many — Buat migrasi untuk tabel pivot
catatan_tagdengan kolomcatatan_iddantag_idsebagai foreign key ke tabel masing-masing. Tambahkan constraintcascadeOnDelete()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.