Langsung ke konten
KamusNgoding
Menengah Laravel 4 menit baca

Eloquent ORM: Model, Migration, dan Relasi di Laravel

#laravel #eloquent #orm #migration #relasi #database #intermediate
📚

Baca dulu sebelum ini:

Pendahuluan

Selama ini kamu mungkin berinteraksi dengan database menggunakan SQL langsung:

SELECT * FROM tasks WHERE user_id = 5 AND done = 0 ORDER BY created_at DESC LIMIT 10;

Laravel punya cara yang jauh lebih elegan. Dengan Eloquent ORM, query di atas menjadi:

$tasks = Task::where('user_id', 5)
             ->where('done', false)
             ->latest()
             ->take(10)
             ->get();

Eloquent mengubah tabel database menjadi objek PHP yang bisa kamu manipulasi secara intuitif. Tidak perlu menulis SQL secara manual untuk operasi umum.

Di artikel ini kita akan membangun fondasi database untuk aplikasi Task Manager:

  • Membuat Model dan Migration
  • Mendefinisikan kolom tabel
  • Operasi CRUD dengan Eloquent
  • Relasi hasMany dan belongsTo
  • Eager Loading untuk menghindari masalah N+1

Eloquent vs Query Builder vs SQL Langsung

PendekatanContohKapan Digunakan
SQL LangsungDB::statement('SELECT...')Query sangat kompleks / legacy
Query BuilderDB::table('tasks')->where(...)Query kompleks tapi tetap fleksibel
Eloquent ORMTask::where(...)->get()Operasi umum CRUD + relasi

Untuk 90% kebutuhan sehari-hari, Eloquent adalah pilihan terbaik.


Membuat Model dan Migration

Perintah Artisan

# Buat Model saja
php artisan make:model Task

# Buat Model + Migration
php artisan make:model Task -m

# Buat Model + Migration + Controller + Resource Controller + Factory + Seeder
php artisan make:model Task -mcrsf

# Yang akan sering kita pakai:
php artisan make:model Task -mc    # Model + Migration + Controller
php artisan make:model Task -mcr   # Model + Migration + Resource Controller

Mari buat dua model untuk proyek Task Manager kita:

php artisan make:model Task -mcr
php artisan make:model Category -mc

Migration: Schema Database

Migration adalah “version control” untuk struktur database. Setiap migration adalah instruksi tentang bagaimana tabel harus dibuat atau diubah.

Migration Task

<?php
// database/migrations/xxxx_xx_xx_create_tasks_table.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('tasks', function (Blueprint $table) {
            $table->id();                                    // Primary key bigint auto_increment
            $table->foreignId('user_id')->constrained()->onDelete('cascade');  // FK ke tabel users
            $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete(); // FK opsional
            $table->string('title');                         // VARCHAR(255)
            $table->text('description')->nullable();         // TEXT, boleh kosong
            $table->enum('priority', ['low', 'medium', 'high'])->default('medium');
            $table->boolean('is_done')->default(false);
            $table->date('due_date')->nullable();
            $table->timestamps();                            // created_at + updated_at
        });
    }

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

Migration Category

<?php
// database/migrations/xxxx_xx_xx_create_categories_table.php

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('name');
            $table->string('color', 7)->default('#6366F1'); // Hex color
            $table->timestamps();
        });
    }

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

Tipe Kolom yang Sering Digunakan

$table->id();                          // BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
$table->string('name');                // VARCHAR(255)
$table->string('slug', 100);           // VARCHAR(100)
$table->text('body');                  // TEXT
$table->longText('content');           // LONGTEXT
$table->integer('count');              // INT
$table->bigInteger('views');           // BIGINT
$table->float('price', 8, 2);         // FLOAT(8,2)
$table->decimal('amount', 10, 2);     // DECIMAL(10,2) — lebih akurat untuk uang
$table->boolean('is_active');          // TINYINT(1)
$table->date('born_at');               // DATE
$table->dateTime('starts_at');         // DATETIME
$table->timestamp('verified_at');      // TIMESTAMP, bisa null
$table->timestamps();                  // created_at + updated_at
$table->softDeletes();                 // deleted_at (untuk soft delete)
$table->enum('status', ['aktif', 'nonaktif']);  // ENUM
$table->json('metadata');              // JSON
$table->foreignId('user_id')->constrained();    // FK + index otomatis

Menjalankan Migration

# Jalankan semua migration yang belum dijalankan
php artisan migrate

# Lihat status migration
php artisan migrate:status

# Rollback migration terakhir
php artisan migrate:rollback

# Rollback 3 batch terakhir
php artisan migrate:rollback --step=3

# Drop semua tabel, lalu migrate dari awal (hati-hati: data hilang!)
php artisan migrate:fresh

# Migrate fresh + jalankan seeder
php artisan migrate:fresh --seed

Model Eloquent

Konfigurasi Model

<?php
// app/Models/Task.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
    use SoftDeletes; // Aktifkan soft delete

    // Kolom yang boleh diisi secara massal (wajib untuk keamanan!)
    protected $fillable = ['title', 'description', 'priority', 'is_done', 'due_date', 'user_id', 'category_id'];

    // Kolom yang TIDAK boleh diisi secara massal
    // protected $guarded = ['id'];  // Alternatif: protect semua kecuali ini

    // Cast tipe data otomatis
    protected $casts = [
        'is_done'  => 'boolean',
        'due_date' => 'date',
    ];

    // Relasi: Task milik satu User
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    // Relasi: Task milik satu Category (opsional)
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }
}
<?php
// app/Models/User.php — tambahkan relasi ke Task

use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Authenticatable
{
    // Relasi: User memiliki banyak Task
    public function tasks(): HasMany
    {
        return $this->hasMany(Task::class);
    }

    // Relasi: User memiliki banyak Category
    public function categories(): HasMany
    {
        return $this->hasMany(Category::class);
    }
}

Operasi CRUD dengan Eloquent

CREATE — Membuat Data Baru

// Cara 1: create() — membutuhkan $fillable di Model
$task = Task::create([
    'title'       => 'Belajar Eloquent',
    'description' => 'Memahami ORM Laravel',
    'priority'    => 'high',
    'user_id'     => auth()->id(),
]);

// Cara 2: save() — lebih manual
$task = new Task();
$task->title = 'Belajar Eloquent';
$task->user_id = auth()->id();
$task->save();

// Cara 3: firstOrCreate — cari atau buat baru
$task = Task::firstOrCreate(
    ['title' => 'Unik Title'],      // kondisi pencarian
    ['priority' => 'low']           // nilai jika dibuat baru
);

READ — Membaca Data

// Ambil semua record
$tasks = Task::all();

// Ambil berdasarkan ID (exception jika tidak ditemukan)
$task = Task::findOrFail(42);

// Ambil satu record dengan kondisi
$task = Task::where('title', 'Belajar')->firstOrFail();

// Query kompleks
$tasks = Task::where('user_id', auth()->id())
             ->where('is_done', false)
             ->where('priority', 'high')
             ->orderBy('due_date', 'asc')
             ->take(5)
             ->get();

// Pagination (15 per halaman)
$tasks = Task::where('user_id', auth()->id())
             ->latest()
             ->paginate(15);

// Select kolom tertentu
$tasks = Task::select('id', 'title', 'is_done')->get();

// Hitung jumlah
$total = Task::where('user_id', auth()->id())->count();
$done  = Task::where('user_id', auth()->id())->where('is_done', true)->count();

UPDATE — Memperbarui Data

// Cara 1: Ambil lalu update
$task = Task::findOrFail(42);
$task->title = 'Judul Baru';
$task->is_done = true;
$task->save();

// Cara 2: update() langsung (lebih efisien untuk banyak record)
Task::where('user_id', auth()->id())
    ->where('is_done', false)
    ->update(['priority' => 'medium']);

// Cara 3: fill() + save()
$task = Task::findOrFail(42);
$task->fill($request->validated());
$task->save();

DELETE — Menghapus Data

// Cara 1: Ambil lalu hapus
$task = Task::findOrFail(42);
$task->delete(); // Jika menggunakan SoftDeletes, hanya menandai deleted_at

// Cara 2: Hapus langsung
Task::destroy(42);               // Hapus berdasarkan ID
Task::destroy([1, 2, 3]);        // Hapus beberapa ID sekaligus

// Cara 3: Hapus dengan kondisi
Task::where('is_done', true)
    ->where('user_id', auth()->id())
    ->delete();

// Hapus permanen (jika menggunakan SoftDeletes)
$task->forceDelete();

// Restore dari soft delete
$task->restore();

Relasi Eloquent

Relasi One-to-Many (HasMany / BelongsTo)

User ──── has many ───→ Task
Task ──── belongs to ─→ User
// Model User sudah punya hasMany (lihat di atas)
// Model Task sudah punya belongsTo (lihat di atas)

// Menggunakan relasi di Controller:
class TaskController extends Controller
{
    public function index()
    {
        // Ambil semua task milik user yang sedang login
        $tasks = auth()->user()->tasks()->latest()->paginate(15);

        return view('tasks.index', compact('tasks'));
    }

    public function store(Request $request)
    {
        // Buat task dan otomatis set user_id
        $task = auth()->user()->tasks()->create($request->validated());

        return redirect()->route('tasks.index')
                         ->with('success', 'Task berhasil ditambahkan!');
    }
}

Mengakses Relasi

$user = User::find(1);

// Akses relasi sebagai property (lazy loading)
$tasks = $user->tasks;                    // Collection of Task
$firstTask = $user->tasks->first();       // Task pertama
$count = $user->tasks->count();           // Jumlah task

// Akses relasi sebagai query builder (untuk tambah kondisi)
$doneTasks = $user->tasks()->where('is_done', true)->get();
$recentTasks = $user->tasks()->latest()->take(5)->get();

// Akses relasi belongsTo
$task = Task::find(1);
$owner = $task->user;           // User pemilik task
$category = $task->category;   // Category task (bisa null)

Eager Loading — Mengatasi Masalah N+1

Masalah N+1 query adalah salah satu masalah performa paling umum di aplikasi Laravel pemula.

Masalah N+1

// ❌ Buruk: Menghasilkan N+1 query (1 untuk tasks, + N untuk setiap user)
$tasks = Task::all(); // Query 1: SELECT * FROM tasks

foreach ($tasks as $task) {
    echo $task->user->name; // Query baru untuk setiap task!
    // Jika ada 100 task → 101 query total!
}

Solusi: Eager Loading dengan with()

// ✅ Baik: Hanya 2 query total (tasks + semua users)
$tasks = Task::with('user')->get();

foreach ($tasks as $task) {
    echo $task->user->name; // Tidak ada query tambahan!
}

// Load beberapa relasi sekaligus
$tasks = Task::with(['user', 'category'])->latest()->paginate(15);

// Nested eager loading (relasi di dalam relasi)
$users = User::with('tasks.category')->get();
// Artinya: load users, dengan tasks, dan setiap task dengan category-nya

Seeder: Data Dummy untuk Development

php artisan make:seeder TaskSeeder
<?php
// database/seeders/TaskSeeder.php

use App\Models\Task;
use App\Models\User;

class TaskSeeder extends Seeder
{
    public function run(): void
    {
        $user = User::first();

        // Buat 20 task dummy
        for ($i = 1; $i <= 20; $i++) {
            Task::create([
                'user_id'     => $user->id,
                'title'       => "Task nomor {$i}",
                'description' => "Deskripsi untuk task {$i}",
                'priority'    => ['low', 'medium', 'high'][rand(0, 2)],
                'is_done'     => rand(0, 1),
            ]);
        }
    }
}
# Jalankan seeder
php artisan db:seed --class=TaskSeeder

# Atau reset database + jalankan semua seeder
php artisan migrate:fresh --seed

Troubleshooting: Error yang Sering Muncul

MassAssignmentException: ... is not mass assignable

Penyebab: Kamu menggunakan create() atau fill() untuk mengisi kolom yang tidak ada di $fillable.

Solusi:

// app/Models/Task.php
protected $fillable = ['title', 'description', 'priority', 'is_done', 'due_date', 'user_id', 'category_id'];
// Pastikan semua kolom yang ingin diisi massal ada di $fillable

SQLSTATE[23000]: Integrity Constraint Violation (Migration Conflict)

Penyebab: Foreign key reference ke tabel yang belum ada, atau urutan migration salah.

Solusi:

# Periksa urutan timestamp migration
# Tabel parent harus dibuat lebih dulu daripada tabel child
# Format timestamp: 2024_01_01_000000 (harus incremental)

# Jika sudah terlanjur, reset dan ulangi
php artisan migrate:fresh

Relasi Mengembalikan null Padahal Data Ada

Penyebab: Nama foreign key tidak mengikuti konvensi Laravel (snake_case + _id), atau relasi tidak di-eager load.

Solusi:

// Jika nama FK tidak standar, tentukan secara eksplisit
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'owner_id'); // custom FK
}

// Pastikan menggunakan eager loading
$tasks = Task::with('user')->get(); // Bukan Task::all()

Pertanyaan yang Sering Diajukan

Apa bedanya Eloquent $fillable vs $guarded?

$fillable adalah whitelist — hanya kolom yang terdaftar yang bisa diisi secara massal. $guarded adalah blacklist — semua kolom bisa diisi kecuali yang terdaftar. Untuk keamanan maksimal, gunakan $fillable dan daftarkan kolom secara eksplisit. Jangan gunakan $guarded = [] di production karena membuka celah mass assignment.

Apa itu N+1 query problem?

N+1 terjadi ketika kamu menjalankan 1 query untuk mengambil daftar, lalu N query tambahan untuk setiap item (biasanya untuk mengakses relasi). Dengan 100 task, ini jadi 101 query! Selalu gunakan eager loading (with()) saat mengakses relasi dalam loop. Baca lebih lanjut di artikel tentang optimasi query database.

Apa itu soft delete dan kapan menggunakannya?

Soft delete “menghapus” record dengan mengisi kolom deleted_at daripada benar-benar menghapus dari database. Sangat berguna untuk: data audit trail, kemampuan restore data, dan mencegah referensi rusak (foreign key). Aktifkan dengan use SoftDeletes di Model dan tambahkan $table->softDeletes() di migration.

Apa itu seeder dan factory?

Seeder mengisi database dengan data awal/dummy menggunakan php artisan db:seed. Factory membuat data palsu yang realistis menggunakan Faker library — sangat berguna untuk testing. Keduanya membantu development tanpa perlu input data manual.


Kesimpulan

Eloquent ORM adalah jantung dari aplikasi Laravel. Kita sudah menguasai:

  • Migration untuk mendefinisikan struktur tabel secara terprogram
  • Model dengan $fillable dan $casts untuk keamanan
  • CRUD Eloquent: create(), find(), update(), delete()
  • Relasi: hasMany / belongsTo untuk menghubungkan tabel
  • Eager Loading dengan with() untuk mencegah N+1 query
  • Seeder untuk data dummy development

Dengan dasar ini, di artikel berikutnya kita siap membangun Aplikasi CRUD Lengkap — Task Manager yang fully functional dengan form validasi, flash message, dan navigasi lengkap!

Database kamu sudah siap. Saatnya bangun aplikasi nyata! 🗄️

Artikel Terkait