BAB 19: Eloquent ORM

ORM Eloquent — cara elegan berinteraksi dengan database menggunakan model PHP di Laravel.

Di bab sebelumnya kita menggunakan Query Builder untuk berinteraksi dengan tabel catatan — merangkai method where, orderBy, dan insert yang semuanya menghasilkan SQL yang aman. Hasilnya adalah koleksi objek stdClass biasa: data sudah ada, tapi tidak punya perilaku apapun.

Eloquent ORM mengangkat level abstraksi satu tingkat lebih tinggi. Alih-alih berpikir dalam konteks tabel dan kolom, kamu berpikir dalam konteks model — objek PHP yang merepresentasikan sebuah baris di database, lengkap dengan method, relasi, dan konvensi yang membuat kode lebih ekspresif. Di balik layar, Eloquent tetap menggunakan Query Builder; bedanya, sekarang kamu jarang perlu menyentuhnya secara langsung.

Membuat Model

Cara tercepat membuat model adalah via Artisan:

php artisan make:model Catatan

Perintah ini menghasilkan file app/Models/Catatan.php. Karena kita sudah punya tabel catatan dari bab sebelumnya, model ini langsung bisa digunakan tanpa konfigurasi tambahan:

// app/Models/Catatan.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Catatan extends Model
{
    //
}

Eloquent menerapkan konvensi penamaan: model Catatan secara otomatis terhubung ke tabel catatan (snake_case, plural). Jika nama tabelmu tidak mengikuti konvensi ini, deklarasikan secara eksplisit:

protected $table = 'notes_data';

Secara default, Eloquent mengasumsikan primary key bernama id dan bertipe integer auto-increment. Kolom created_at dan updated_at dikelola otomatis setiap kali record disimpan atau diperbarui.

Mass Assignment dan Fillable

Sebelum bisa membuat atau memperbarui record menggunakan array, kamu perlu mendefinisikan atribut mana yang boleh diisi secara massal. Ini adalah perlindungan dari mass assignment vulnerability — situasi di mana penyerang bisa menyisipkan kolom berbahaya (seperti is_admin) ke dalam request.

Ada dua pendekatan: $fillable (whitelist) atau $guarded (blacklist):

// app/Models/Catatan.php
class Catatan extends Model
{
    // Hanya kolom ini yang boleh diisi via create() atau fill()
    protected $fillable = [
        'user_id',
        'judul',
        'isi',
        'prioritas',
        'selesai',
    ];
}

Dengan $fillable terdefinisi, Eloquent akan menolak senyap atribut apapun yang tidak ada dalam daftar tersebut saat operasi mass assignment.

Mengambil Data

Eloquent menyediakan beberapa cara untuk mengambil record, masing-masing dengan kegunaan berbeda:

use App\Models\Catatan;

// Ambil semua catatan sebagai Collection
$semuaCatatan = Catatan::all();

// Ambil berdasarkan primary key
$catatan = Catatan::find(5);

// Lempar ModelNotFoundException jika tidak ditemukan
$catatan = Catatan::findOrFail(5);

// Ambil record pertama yang cocok
$catatan = Catatan::where('prioritas', 'tinggi')->first();

// Shorthand untuk where + first
$catatan = Catatan::firstWhere('prioritas', 'tinggi');

// Lempar exception jika tidak ditemukan
$catatan = Catatan::where('user_id', 1)->firstOrFail();

Berbeda dengan Query Builder yang mengembalikan stdClass, Catatan::find(5) mengembalikan instance Catatan. Artinya kamu bisa mengakses atributnya seperti properti objek dan — nanti setelah relasi didefinisikan — mengakses data dari tabel lain dengan cara yang intuitif.

Mengambil dengan Kondisi

Eloquent mewarisi semua kemampuan filtering dari Query Builder:

// Filter berantai
$catatan = Catatan::where('user_id', 1)
    ->where('selesai', false)
    ->orderByDesc('created_at')
    ->limit(10)
    ->get();

// Ambil kolom tertentu
$catatan = Catatan::select('id', 'judul', 'prioritas')
    ->where('user_id', 1)
    ->get();

// Cek keberadaan record
$adaCatatan = Catatan::where('user_id', 1)->exists();
$total      = Catatan::where('user_id', 1)->count();

Perbedaan utama: ketika menggunakan Eloquent, setiap item dalam Collection adalah instance model, bukan stdClass. Ini berarti kamu bisa memanggil method instance seperti $catatan->markAsComplete() jika kamu mendefinisikannya di model.

Membuat Record

Ada dua cara utama membuat record dengan Eloquent: menggunakan instance baru atau menggunakan method statis create().

Menggunakan save()

// Buat instance, isi atribut, lalu simpan
$catatan = new Catatan();
$catatan->user_id  = 1;
$catatan->judul    = 'Belajar Eloquent';
$catatan->isi      = 'Eloquent membuat interaksi database jauh lebih menyenangkan.';
$catatan->prioritas = 'normal';
$catatan->selesai  = false;
$catatan->save();

// Setelah save(), $catatan->id sudah terisi secara otomatis
echo $catatan->id;        // misalnya: 42
echo $catatan->created_at; // timestamp yang baru diisi Eloquent

Menggunakan create()

// Buat dan simpan dalam satu langkah — butuh $fillable terdefinisi
$catatan = Catatan::create([
    'user_id'   => 1,
    'judul'     => 'Deploy ke production',
    'isi'       => 'Ingat untuk cek environment variables sebelum deploy.',
    'prioritas' => 'tinggi',
    'selesai'   => false,
]);

create() mengembalikan instance model yang sudah disimpan, termasuk id yang baru dihasilkan database.

firstOrCreate dan updateOrCreate

Dua method ini sangat berguna untuk menghindari duplikasi data:

// Cari record dengan kondisi; buat jika tidak ditemukan
$catatan = Catatan::firstOrCreate(
    ['user_id' => 1, 'judul' => 'Catatan rutin harian'],
    ['prioritas' => 'normal', 'selesai' => false]
);

// Buat atau update — jika cocok, update dengan array kedua
$catatan = Catatan::updateOrCreate(
    ['user_id' => 1, 'judul' => 'Catatan rutin harian'],
    ['prioritas' => 'tinggi', 'selesai' => false]
);

Parameter pertama adalah kondisi pencarian; parameter kedua adalah nilai yang akan diisi saat membuat (untuk firstOrCreate) atau diperbarui (untuk updateOrCreate).

Memperbarui Record

Memperbarui record yang sudah ada bisa dilakukan dengan beberapa cara:

// Cara 1: ambil, ubah atribut, simpan
$catatan = Catatan::findOrFail(5);
$catatan->selesai  = true;
$catatan->prioritas = 'rendah';
$catatan->save();

// Cara 2: mass update via fill() + save()
$catatan = Catatan::findOrFail(5);
$catatan->fill([
    'selesai'   => true,
    'prioritas' => 'rendah',
]);
$catatan->save();

// Cara 3: mass update langsung — tidak melewati model events
Catatan::where('user_id', 1)
    ->where('selesai', true)
    ->update(['prioritas' => 'arsip']);

update() di Cara 3 adalah query langsung ke database dan tidak memanggil model events (updating, updated). Jika kamu mendefinisikan observer atau event listener pada model, gunakan Cara 1 atau 2 agar event tersebut terpanggil.

Eloquent juga menyediakan cara untuk memeriksa apakah atribut model sudah berubah sebelum disimpan:

$catatan = Catatan::findOrFail(5);
$catatan->judul = 'Judul baru';

$catatan->isDirty();          // true — ada perubahan yang belum disimpan
$catatan->isDirty('judul');   // true
$catatan->isDirty('prioritas'); // false — belum diubah
$catatan->getOriginal('judul'); // nilai sebelum diubah

Menghapus Record

// Hapus satu record
$catatan = Catatan::findOrFail(5);
$catatan->delete();

// Hapus berdasarkan primary key — satu atau lebih
Catatan::destroy(5);
Catatan::destroy([1, 2, 3]);

// Hapus banyak record berdasarkan kondisi
Catatan::where('user_id', 1)->where('selesai', true)->delete();

Sama seperti di Query Builder, delete() tanpa kondisi akan menghapus semua baris di tabel. Selalu pastikan ada klausa where yang tepat.

Soft Delete

Untuk data yang tidak boleh benar-benar dihapus — misalnya karena audit trail atau kemungkinan restore — Eloquent menyediakan soft delete. Alih-alih menghapus baris, Eloquent hanya mengisi kolom deleted_at.

Tambahkan kolom ke migrasi:

// Di file migrasi
$table->softDeletes(); // menambahkan kolom deleted_at nullable

Aktifkan di model:

use Illuminate\Database\Eloquent\SoftDeletes;

class Catatan extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'user_id', 'judul', 'isi', 'prioritas', 'selesai',
    ];
}

Sekarang ketika $catatan->delete() dipanggil, baris tidak dihapus — deleted_at diisi dengan timestamp saat ini. Secara default, semua query Eloquent akan mengabaikan record yang sudah soft-deleted:

// Hanya mengembalikan catatan yang belum dihapus (default)
$catatan = Catatan::all();

// Termasuk yang sudah soft-deleted
$catatan = Catatan::withTrashed()->get();

// Hanya yang sudah soft-deleted
$catatan = Catatan::onlyTrashed()->get();

// Kembalikan record yang sudah soft-deleted
$catatan = Catatan::withTrashed()->find(5);
$catatan->restore();

// Hapus permanen
$catatan->forceDelete();

Local Scopes

Ketika kondisi query yang sama digunakan berulang kali di berbagai tempat, jadikan ia scope di model untuk menghindari duplikasi:

// app/Models/Catatan.php
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;

class Catatan extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'user_id', 'judul', 'isi', 'prioritas', 'selesai',
    ];

    #[Scope]
    protected function belumSelesai(Builder $query): void
    {
        $query->where('selesai', false);
    }

    #[Scope]
    protected function prioritasTinggi(Builder $query): void
    {
        $query->where('prioritas', 'tinggi');
    }

    #[Scope]
    protected function milikUser(Builder $query, int $userId): void
    {
        $query->where('user_id', $userId);
    }
}

Penggunaan scope terasa seperti method biasa yang bisa dirantai:

// Query yang bersih dan terbaca
$catatan = Catatan::belumSelesai()
    ->prioritasTinggi()
    ->milikUser(1)
    ->orderByDesc('created_at')
    ->get();

Bandingkan dengan tanpa scope:

// Tanpa scope — perlu ditulis ulang di setiap tempat
$catatan = Catatan::where('selesai', false)
    ->where('prioritas', 'tinggi')
    ->where('user_id', 1)
    ->orderByDesc('created_at')
    ->get();

Scope juga bisa menerima parameter seperti milikUser() di atas, sehingga tetap fleksibel meski kondisinya dinamis.

Model Events

Eloquent memancarkan event di berbagai titik siklus hidup model: creating, created, updating, updated, saving, saved, deleting, deleted. Kamu bisa hook ke event ini langsung di model menggunakan method booted():

protected static function booted(): void
{
    // Setiap kali catatan baru dibuat, log aktivitasnya
    static::created(function (Catatan $catatan) {
        \Log::info("Catatan baru dibuat: {$catatan->judul}");
    });

    // Sebelum dihapus, periksa kondisi tertentu
    static::deleting(function (Catatan $catatan) {
        // bisa melempar exception untuk mencegah penghapusan
    });
}

Untuk logika yang lebih kompleks atau jika banyak event perlu dikelola, gunakan Observer — class terpisah yang menampung semua event handler untuk satu model:

php artisan make:observer CatatanObserver --model=Catatan
// app/Observers/CatatanObserver.php
class CatatanObserver
{
    public function created(Catatan $catatan): void
    {
        // dipanggil setelah record dibuat
    }

    public function updated(Catatan $catatan): void
    {
        // dipanggil setelah record diperbarui
    }

    public function deleted(Catatan $catatan): void
    {
        // dipanggil setelah record dihapus
    }
}

Daftarkan observer di model menggunakan attribute:

use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy([CatatanObserver::class])]
class Catatan extends Model
{
    // ...
}

Latihan

Kerjakan latihan berikut menggunakan Tinker atau di dalam controller:

  1. Scope dan filter — Tambahkan scope mendesak() ke model Catatan yang memfilter catatan dengan prioritas = 'mendesak'. Kemudian buat query yang menggabungkan scope belumSelesai() dan mendesak() untuk satu user tertentu.

  2. firstOrCreate di controller — Di controller yang sudah kamu buat di bab-bab sebelumnya, ganti operasi DB::table('catatan')->insert() dengan Catatan::create(). Pastikan $fillable sudah didefinisikan dengan benar sehingga tidak ada atribut yang tertolak.

  3. Soft delete — Tambahkan soft delete ke model Catatan. Jalankan migrasi baru yang menambahkan kolom deleted_at. Hapus beberapa record, lalu verifikasi bahwa query biasa tidak menampilkan record yang sudah dihapus, tapi withTrashed() menampilkannya.

Penutup Bab

Eloquent ORM mengubah cara kamu berinteraksi dengan database dari “tulis SQL” menjadi “operasikan objek”. Model bukan hanya wadah data — ia bisa punya scope, event, observer, dan segera kita akan tambahkan relasi ke model lain. create(), update(), delete() terasa alami karena itulah cara kita memikirkan domain aplikasi, bukan cara database memikirkan tabel dan baris.

Namun Eloquent baru menunjukkan kekuatan sesungguhnya ketika model-model saling terhubung. Data Catatan yang kita gunakan punya user_id yang merujuk ke tabel users, tapi sampai sejauh ini kita masih harus join tabel secara manual untuk mengakses data user. Bab selanjutnya memperkenalkan fitur relasi Eloquent — cara mendefinisikan hubungan antar model sehingga mengakses data yang berelasi menjadi sesederhana memanggil properti.

Referensi

  1. 1Eloquent: Getting Started — Laravel 12.x Documentation
  2. 2Eloquent: Soft Deleting — Laravel 12.x Documentation
  3. 3Eloquent: Observers — Laravel 12.x Documentation