Belajar Laravel #6: Eloquent ORM Dasar — Berbicara dengan Database Lewat Objek PHP

Hari ini saya masuk ke materi yang paling saya tunggu-tunggu sejak mulai belajar Laravel: Eloquent ORM. Setelah di episode kemarin saya belajar migration untuk membangun struktur tabel, kali ini saya mulai berkenalan dengan cara Laravel “berbicara” dengan database — dan jujur, ini momen di mana Laravel terasa benar-benar berbeda dari cara saya menulis SQL manual dulu.

Dulu: SQL di tangan, sekarang: objek di tangan

Waktu saya masih ngoding PHP tanpa framework, query database selalu berbentuk teks: INSERT INTO todos, SELECT * FROM todos, dan seterusnya. Setiap operasi saya harus menulis string SQL, memastikan sintaksnya benar, lalu memetakan hasilnya ke array secara manual. Rajin sih, tapi banyak bagian yang berulang dan rawan salah ketik.

Eloquent mengubah pendekatan itu. Alih-alih menulis SQL, saya bekerja dengan objek PHP. Satu baris di tabel = satu objek model. Satu tabel = satu class model. Inilah yang disebut ORM (Object-Relational Mapping): jembatan yang memetakan baris database menjadi objek di kode.

Ilustrasi relasi model Eloquent dengan tabel database dan operasi CRUD
Diagram yang saya buat untuk mengingat: Model = tabel, satu baris = satu objek

Model: class kecil yang tahu nama tabelnya

Membuat model di Laravel hanya butuh satu perintah Artisan:

php artisan make:model Todo

File app/Models/Todo.php yang dihasilkan sangat sederhana:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Todo extends Model
{
    //
}

Yang menarik bagi saya: class ini kosong, tapi sudah langsung bisa dipakai. Laravel memakai konvensi penamaan — model Todo secara otomatis terhubung ke tabel todos (jamak dari nama class), dengan primary key id. Konvensi ini yang membuat kode tetap rapi tanpa harus deklarasi ulang di setiap class.

CRUD dengan gaya baru

Berikut yang saya catat sebagai pola dasar CRUD lewat Eloquent:

// CREATE
$todo = Todo::create([
    'title' => 'Belajar Eloquent ORM',
    'completed' => false,
]);

// READ
$semuaTodo = Todo::all();              // semua baris
$satuTodo = Todo::find(1);             // cari by primary key

// UPDATE
$todo->completed = true;
$todo->save();

// DELETE
$todo->delete();

Beberapa hal yang saya pelajari dari contoh di atas:

  • create() butuh data dalam bentuk array asosiatif; kolom lain yang tidak diisi memakai nilai default dari migration.
  • find() mengembalikan satu objek model, atau null kalau id-nya tidak ada.
  • Untuk update, saya ubah property objeknya lalu panggil save() — pola ini terasa natural seperti mengedit variabel biasa.
  • Kolom created_at dan updated_at diisi otomatis oleh Eloquent (lewat timestamps), jadi saya tidak perlu menuliskannya manual.

Yang baru saya sadari (dan sempat bikin bingung)

Hal yang sempat membuat saya tersendat: Todo::create() menolak data kalau field-nya tidak terdaftar di property $fillable. Ini proteksi mass assignment — mencegah user melempar kolom tersembunyi lewat form (misalnya menaikkan role jadi admin). Jadi saya perlu menambahkan:

class Todo extends Model
{
    protected $fillable = ['title', 'completed'];
}

Awalnya saya mengira ini merepotkan, tapi setelah dipahami justru membuat saya lebih disiplin: setiap field yang boleh diisi user dicatat eksplisit, sisanya tertutup. Kebalikan dari $fillable adalah $guarded, dan salah satu dari keduanya biasanya dideklarasikan di model.

Contoh paling gampang yang saya bayangkan: Todo::all() itu seperti “saya minta semua baris tabel todos, dan Laravel mengembalikannya sebagai kumpulan objek Todo yang bisa saya loop satu per satu”. Hasilnya bukan array asosiatif polos seperti kebiasaan lama saya, melainkan koleksi objek — dan dari situ saya bisa memanggil method pada tiap objek, misalnya mengecek $todo->completed atau memanggil relasi yang nanti akan saya pelajari.

Satu tips yang saya dapatkan dari dokumentasi: kalau ragu kolom mana yang diisi otomatis, saya bisa cek model Todo di tinker dengan menulis Todo::first() lalu melihat atributnya. php artisan tinker menjadi “teman ngobrol” saya untuk mencoba query tanpa menulis controller dulu — sangat menolong saat masih tahap mengenal Eloquent seperti sekarang.

Accessor, Mutator, dan Casting — Memformat Data Secara Otomatis

Salah satu kekuatan Eloquent yang baru saya sadari adalah kemampuan memformat data secara otomatis lewat Accessor dan Mutator. Misalnya, saya punya kolom password yang harus selalu di-hash sebelum disimpan, atau kolom full_name yang saya gabungkan dari first_name dan last_name.

// Accessor: format saat data DIBACA
class User extends Model
{
    protected $fillable = ['name', 'email', 'is_admin'];

    // Accessor baru
    public function getFullNameAttribute()
    {
        return $this->first_name . ' ' . $this->last_name;
    }

    // Mutator: format saat data DITULIS
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }
}

// Penggunaan
$user = User::find(1);
echo $user->full_name;  // "Rudi Hartadi"
$user->password = 'rahasia'; // otomatis di-hash

Untuk pemformatan yang lebih deklaratif, Laravel menyediakan attribute casting. Saya cukup mendeklarasikan kolom apa yang perlu di-cast, dan Eloquent menangani sisanya:

class Todo extends Model
{
    protected $fillable = ['title', 'completed', 'metadata', 'deleted_at'];

    protected $casts = [
        'completed' => 'boolean',       // integer 0/1 jadi true/false
        'metadata'  => 'array',         // JSON string jadi array PHP
        'settings'  => 'json',          // simpan sebagai JSON di DB
        'deleted_at' => 'datetime',     // otomatis parse ke Carbon
    ];

    // Laravel 10+ juga mendukung encrypted casting
    // 'secret' => 'encrypted',
}

Yang paling membantu untuk saya: array cast. Kolom di database berupa string JSON, tapi di kode saya langsung dapat array PHP tanpa manual json_decode(). Praktis dan mengurangi boilerplate.

Query Scopes — Membungkus Query yang Sering Dipakai

Seiring aplikasi berkembang, saya menemukan query yang sama ditulis berulang kali — misalnya filter todo yang belum selesai. Eloquent menyediakan query scopes untuk membungkusnya menjadi method yang bisa dipanggil kapan saja:

// Local Scope — didefinisikan di model
class Todo extends Model
{
    public function scopeIncomplete($query)
    {
        return $query->where('completed', false);
    }

    public function scopeRecent($query, $days = 7)
    {
        return $query->where('created_at', '>=', now()->subDays($days));
    }
}

// Penggunaan
$recentUnfinished = Todo::incomplete()->recent(3)->get();

Untuk scope yang berlaku secara global di semua query model, saya bisa menggunakan global scope:

class Todo extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope('active', function ($query) {
            $query->where('is_archived', false);
        });
    }
}

// Sekarang Todo::all() otomatis hanya mengambil data aktif
// Kalau perlu ambil semua termasuk archived:
Todo::withoutGlobalScope('active')->get();

Saya baru tahu bahwa global scope ini mirip dengan “default filter” yang selalu aktif — membuat kode lebih bersih karena filter tidak perlu ditulis di setiap controller.

Soft Deletes — Menghapus Tanpa Benar-benar Hapus

Di awal saya mengira $todo->delete() akan menghapus baris dari database secara permanen. Ternyata Laravel menyediakan opsi Soft Deletes — data ditandai sebagai “dihapus” tapi masih tersimpan di tabel, sehingga bisa dipulihkan.

// 1. Migration: tambahkan kolom deleted_at
$table->softDeletes();

// 2. Model: gunakan SoftDeletes trait
use Illuminate\\Database\\Eloquent\\SoftDeletes;

class Todo extends Model
{
    use SoftDeletes;

    protected $dates = ['deleted_at']; // atau $casts di Laravel 8+
}

// 3. Usage
$todo->delete();          // ditandai, bukan dihapus permanen
$todo->trashed();         // cek apakah soft-deleted
Todo::withTrashed()->get(); // ambil semua, termasuk yang terhapus
$todo->restore();         // pulihkan kembali

Saya belajar bahwa soft deletes sangat berguna untuk data penting — misalnya transaksi atau catatan user. Lebih baik menandai daripada menghapus, karena data bisa dipulihkan kalau sewaktu-waktu dibutuhkan.

N+1 Problem — Musuh Performa yang Saya Pelajari dengan Cara yang Salah

Satu kesalahan yang sempat saya buat: saya looping semua todo, lalu di dalam loop saya panggil $todo->user->name. Ternyata ini memicu N+1 query problem — satu query untuk mengambil semua todo, ditambah satu query terpisah untuk setiap user di setiap todo. Kalau ada 100 todo, itu berarti 101 query ke database.

// ❌ N+1 problem
$todos = Todo::all();
foreach ($todos as $todo) {
    echo $todo->user->name; // query terpisah setiap iterasi
}

// ✅ Eager loading dengan with()
$todos = Todo::with('user')->get(); // hanya 2 query
foreach ($todos as $todo) {
    echo $todo->user->name; // tidak ada query tambahan
}

// Debug: cek jumlah query
DB::enableQueryLog();
Todo::with('user')->get();
dd(DB::getQueryLog());

Pelajaran saya: kalau ada relasi yang akan diakses di loop, selalu pakai with(). Laravel juga menyediakan Model::preventLazyLoading() di environment development untuk otomatis melempar exception kalau ada N+1 query yang terlewat.

Model Events — Hook di Momen Penting

Eloquent menyediakan model events yang memungkinkan saya menjalankan kode di momen-momen tertentu: sebelum/sesudah data di-create, update, atau delete. Ini berguna untuk log otomatis atau validasi tambahan.

class Todo extends Model
{
    protected static function booted(): void
    {
        static::creating(function (Todo $todo) {
            $todo->slug = Str::slug($todo->title);
        });

        static::saved(function (Todo $todo) {
            Log::info("Todo {$todo->id} telah disimpan");
        });

        static::deleted(function (Todo $todo) {
            Cache::forget('todos-list');
        });
    }
}

// Events yang tersedia: creating, created, updating, updated,
// saving, saved, deleting, deleted, restoring, restored

Events ini menghemat banyak kode karena saya tidak perlu mengulang logika yang sama di setiap controller. Sekali didefinisikan di model, otomatis berlaku di mana pun model dipanggil.

Eloquent vs Query Builder — Kapan Pakai Mana

Saya sempat bingung kapan harus pakai Eloquent Model dan kapan cukup Query Builder. Berikut ringkasan yang saya buat:

AspekEloquent ModelQuery Builder
RetrievalTodo::where('done', true)->get()DB::table('todos')->where('done', true)->get()
HasilKoleksi objek modelKoleksi objekstdClass
RelasiOtomatis (with())Manual (join)
Accessors/MutatorsTersediaTidak tersedia
EventsTersediaTidak tersedia
PerformaSedikit lebih lambat (overhead objek)Lebih cepat untuk query kompleks
Cocok untukCRUD, relasi, validasiQuery kompleks, reporting, raw SQL

Prinsip yang saya pegang: kalau data butuh relasi, accessors, atau event, pakai Eloquent. Kalau query sangat kompleks atau hanya butuh data ringan tanpa relasi, Query Builder lebih efisien. Dalam praktiknya, saya lebih sering pakai Eloquent karena kemudahannya.

Referensi Cepat: CRUD Methods pada Eloquent

Ini tabel singkat yang saya buat sebagai cheat sheet supaya tidak perlu bolak-balik ke dokumentasi:

OperasiMethodContoh
Createcreate() / new + save()Todo::create(['title' => 'Belajar'])
Read Allall() / get()Todo::all()
Read Onefind() / findOrFail()Todo::find(1)
Read Filteredwhere() + get()Todo::where('done', true)->get()
Updateupdate() / save()$todo->update(['done' => true])
Deletedelete() / destroy()Todo::destroy(1)
Paginatepaginate()Todo::paginate(15)
Countcount()Todo::where('done', false)->count()
Existsexists()Todo::where('title', 'Belajar')->exists()

Catatan Pribadi: Pengalaman Pertama Pakai ORM

Jujur, saat pertama kali menggunakan Eloquent saya sempat ragu — apakah ini benar-benar lebih baik daripada menulis SQL langsung? Setelah beberapa minggu, saya yakin jawabannya ya. Kode jadi lebih singkat, lebih mudah dibaca, dan yang paling penting: saya tidak perlu lagi bolak-balik ke phpMyAdmin hanya untuk menguji query.

Tapi ada satu hal yang perlu saya ingat terus: Eloquent itu penghubung, bukan pengganti SQL. Ketika saya menghadapi query yang sangat kompleks — misalnya untuk laporan dengan subquery dan aggregate functions — saya kembali ke Query Builder atau bahkan raw SQL. Eloquent memberi kemudahan untuk 90% kasus harian, dan itu sudah cukup untuk membuat saya lebih produktif.

Kenapa ini penting buat saya

Buat saya, Eloquent adalah titik di mana Laravel mulai “mengerti” database tanpa saya harus menulis SQL di setiap controller. Kode jadi lebih pendek, lebih mudah dibaca, dan risiko typo SQL berkurang drastis. Ini bukan berarti SQL tidak perlu dipelajari — pemahaman query tetap penting — tapi untuk kebutuhan CRUD harian, Eloquent sangat menghemat waktu.

Di episode berikutnya saya akan lanjut ke relasi antar-tabel (hasOne, hasMany, belongsTo) — di situlah Eloquent benar-benar menunjukkan kekuatannya. Sampai ketemu!

Sumber

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *

Situs ini menggunakan Akismet untuk mengurangi spam. Pelajari bagaimana data komentar Anda diproses