Sepanjang Section Database, kita menulis SQL sendiri dengan PDO: menyusun string query, mengikat parameter, lalu memetakan baris hasil ke array atau object. Eloquent, ORM resmi Laravel, mengambil alih pekerjaan pemetaan tersebut — class PHP menjadi perwakilan tabel, dan instance menjadi perwakilan baris. Pada chapter ini kita mempelajari anatomi model, migrasi skema, query builder, proteksi mass assignment, relasi antar model, hingga eager loading sebagai solusi masalah N+1. Seluruh konsepnya membangun langsung di atas SQL dan JOIN yang sudah kita kuasai, bukan menggantikannya.

Konsep ORM Active Record #

Eloquent mengikuti pola Active Record: satu class mewakili satu tabel, satu instance mewakili satu baris, dan method instance menyimpan atau mengubah baris tersebut. Class Post terhubung ke tabel posts, $post->title membaca kolom title, dan $post->save() menjalankan INSERT atau UPDATE tanpa kita menyusun SQL. Pola ini berbeda dari repository pattern yang kita bahas di Repository Pattern — di sana logika akses data dipisahkan ke class tersendiri. Active Record memilih jalan lain: model sekaligus menjadi pintu baca-tulis data, sehingga kode aplikasi lebih ringkas dengan taruhan batas arsitektur yang lebih tebal.

Anatomi Model Eloquent #

Model Eloquent adalah class PHP biasa yang mewarisi Illuminate\Database\Eloquent\Model. Isinya bisa sesederhana satu baris deklarasi $fillable, karena hampir semua perilaku ditentukan konvensi nama.

php
<?php
// app/Models/Post.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Kolom yang boleh diisi melalui mass assignment.
     */
    protected $fillable = ['title', 'body', 'author_id'];
}

Empat konvensi bekerja di balik class singkat ini. Pertama, nama class tunggal bahasa Inggris Post dipetakan ke tabel bentuk jamak posts. Kedua, primary key diasumsikan bernama id. Ketiga, Eloquent mengelola dua kolom waktu created_at dan updated_at secara otomatis setiap kali baris disimpan. Keempat, $fillable mendeklarasikan kolom mana saja yang boleh diisi serentak — daftar putih yang perannya kita bedah di bagian mass assignment di bawah. Bila konvensi tidak cocok, semuanya bisa ditimpa: protected $table, protected $primaryKey, hingga public $timestamps = false.

Membuat Tabel dengan Migration #

Model tanpa tabel tidak berguna, dan Laravel mengelola skema tabel lewat migration — berkas PHP berurutan yang versi-kontrolnya setara dengan yang kita bangun di Schema Migrations . Kerangka migrasi dibuat lewat Artisan, lalu skema ditulis dengan Blueprint API.

bash
php artisan make:migration create_posts_table --create=posts
php artisan migrate
php
<?php
// database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->foreignId('author_id')->constrained();
            $table->timestamps();
        });
    }

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

Mari kita baca skema di atas kolom per kolom:

  1. $table->id(): membuat id bertipe BIGINT unsigned, auto-increment, dan primary key — padanan INTEGER PRIMARY KEY AUTOINCREMENT di SQLite yang kita tulis manual.
  2. $table->string('title'): kolom VARCHAR (default 255) dengan NOT NULL.
  3. $table->foreignId('author_id')->constrained(): kolom foreign key BIGINT plus constraint yang merujuk tabel authors — relasi yang sama dengan PRIMARY DAN FOREIGN KEYS di database modeling .
  4. $table->timestamps(): dua kolom created_at dan updated_at nullable.

Perintah php artisan migrate menjalankan method up() dari seluruh migrasi yang belum diterapkan, mencatat nomor urutnya di tabel khusus, dan php artisan migrate:rollback memanggil down() untuk satu batch terakhir. Karena setiap driver database punya dialek SQL yang berbeda, Blueprint menerjemahkan pemanggilan method menjadi DDL yang sesuai — kode migrasi yang sama menghasilkan skema SQLite di laptop dan MySQL di produksi.

Query Eloquent vs SQL Mentah #

Setiap pemanggilan Eloquent pada akhirnya tetap menghasilkan SQL. Membandingkan keduanya berdampingan adalah cara tercepat memahami apa yang dilakukan Eloquent.

php
<?php
$posts = Post::where('status', 'published')
    ->orderBy('created_at', 'desc')
    ->limit(5)
    ->get();
sql
-- SQL yang dirakit oleh kode di atas
SELECT * FROM posts
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT 5;

Nilai 'published' tidak pernah digabungkan ke string SQL — query builder menggunakannya sebagai parameter terikat, persis teknik Parameter Binding yang melindungi kita dari SQL injection. Hasil get() adalah collection berisi instance Post, sehingga $posts->first()->title langsung tersedia tanpa pemetaan manual dari array baris. Operasi sehari-hari lain memiliki padanan yang sama lugas:

OperasiEloquentSQL yang setara
Semua barisPost::all()SELECT * FROM posts
Cari per kunciPost::find(1)SELECT * FROM posts WHERE id = 1
Simpan baru$post->save()INSERT INTO posts ...
Ubah baris$post->save()UPDATE posts ... WHERE id = ?
Hapus barisPost::destroy(1)DELETE FROM posts WHERE id = 1

Alur create-read-update-delete lengkap dengan instance terlihat seperti ini:

php
<?php
// INSERT: buat instance baru lalu simpan
$post = new Post();
$post->title = 'Belajar Eloquent';
$post->body = 'Isi artikel pertama tentang ORM.';
$post->author_id = 1;
$post->save();

// SELECT: cari berdasarkan primary key
$post = Post::find(1);

// UPDATE: ubah atribut lalu simpan kembali
$post->title = 'Judul yang Lebih Baik';
$post->save();

// DELETE
Post::destroy(1);

Perhatikan bahwa method save() yang sama menjalankan INSERT saat instance baru dan UPDATE saat instance diambil dari database — Eloquent mengetahui perbedaannya dari tanda bahwa instance sebelumnya pernah dimuat.

Mass Assignment dan Proteksi Fillable #

Menulis atribut satu per satu menjadi cepat melelahkan, sehingga Eloquent menyediakan create() yang menerima array sekaligus. Di sinilah proteksi $fillable bekerja: hanya kolom yang terdaftar yang diterima, sisanya diabaikan atau menimbulkan exception tergantung konfigurasi.

php
<?php
// create() mengisi atribut sekaligus lalu menyimpan barisnya
$post = Post::create([
    'title' => 'Belajar Eloquent',
    'body' => 'Isi artikel pertama tentang ORM.',
    'author_id' => 1,
]);

Kombinasi yang sehat memakai dua lapis: validasi input di controller menyaring kunci yang diharapkan, lalu $fillable menegaskan kolom yang boleh ditulis. Dua lapis ini saling melengkapi — validasi berbicara tentang bentuk permintaan, $fillable berbicara tentang batas model.

Relasi hasMany dan belongsTo #

Relasi tabel yang dulu kita tulis dengan JOIN kini dideklarasikan sebagai method pada model. Seorang penulis memiliki banyak artikel; sebuah artikel dimiliki satu penulis — pasangan hasMany dan belongsTo mengungkapkan relasi satu-ke-banyak dari kedua arah.

php
<?php
// app/Models/Author.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
php
<?php
// app/Models/Post.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title', 'body', 'author_id'];

    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}
php
<?php
// Pemakaian relasi di controller
$author = Author::find(1);

foreach ($author->posts as $post) {
    echo $post->title . "\n";
}

$post = Post::find(1);
echo $post->author->name . "\n";

Tiga hal penting terjadi pada blok pemakaian di atas. Pertama, hasMany(Post::class) membaca konvensi foreign key author_id pada tabel posts — sama dengan JOIN authors.id = posts.author_id di Dasar SQL , hanya dinyatakan dari sisi object. Kedua, relasi dipanggil sebagai property ($author->posts), dan Eloquent menjalankan query relasi secara lazy pada akses pertama lalu menyimpan hasilnya. Ketiga, $post->author->name adalah dua query terpisah (ambil post, ambil author), bukan satu JOIN — perilaku yang membuka pintu masalah khas di bagian berikutnya.

Eager Loading dan Masalah N+1 #

Masalah N+1 lahir dari pola lazy loading: satu query mengambil N baris induk, lalu setiap aksesan relasi memicu query tambahan. Untuk 100 artikel, halaman daftar penulis menjalankan 101 query — persis pola boros yang kita analisis di Query Optimization . Solusi Eloquent adalah eager loading lewat with().

php
<?php
// Lazy loading: 1 query untuk posts + 1 query author PER BARIS
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name . "\n";
}

// Eager loading: hanya 2 query untuk semua baris
$posts = Post::with('author')->get();
foreach ($posts as $post) {
    echo $post->author->name . "\n";
}

Panggilan with('author') menjalankan query kedua berbentuk SELECT * FROM authors WHERE id IN (...) yang memuat seluruh penulis sekaligus, lalu menautkannya ke masing-masing artikel di memori. Access $post->author kini membaca dari relasi yang sudah terisi sehingga tidak ada query tambahan di dalam loop. with() juga menerima beberapa relasi sekaligus (with('author', 'comments')) dan relasi bersarang (with('author.profile')).

Eloquent Collection #

Hasil get() dan all() bukan array polos, melainkan Illuminate\Support\Collection — wrapper object di atas array yang menyediakan puluhan method pemrosesan. Ini versi berkelas dari array_map dan array_filter yang kita dalami di Array Map, Filter, Reduce .

php
<?php
$posts = Post::where('status', 'published')->get();

$judul = $posts->pluck('title');
$totalViews = $posts->sum('views');
$populer = $posts->where('views', '>', 1000);
$ada = $posts->contains('title', 'Belajar Eloquent');

Bedakan dua lapis dengan cermat: Post::where(...) sebelum get() adalah query builder yang merakit SQL, sedangkan $posts->where(...) setelah get() adalah penyaringan di memori atas data yang sudah terambil. Keduanya sah — penyaringan database untuk data besar dan pemrosesan collection untuk data yang sudah ada di tangan. Eloquent collection juga bisa diubah ke array biasa dengan ->all() atau ->toArray() ketika kita perlu memakai fungsi array native.

Ringkasan #

Eloquent memetakan tabel ke class dan baris ke instance, mengelola skema lewat migration, merakit SQL berparameter melalui query builder, menjaga batas penulisan kolom dengan $fillable, mendeklarasikan relasi sebagai method, dan menuntaskan N+1 lewat eager loading. Di balik semua kenyamanan itu, SQL yang dihasilkan tetap SQL yang kita kenal — artinya intuisi database dari section sebelumnya tetap menjadi alat diagnosis utama. Sebelum data disimpan, satu pertahanan terakhir wajib berdiri: memastikan data yang masuk valid. Itulah topik chapter berikutnya tentang validation di Laravel.

Referensi #