Developer yang baru pindah dari PHP native ke Laravel sering merasa Eloquent terlalu "ajaib": satu baris kode bisa menjalankan banyak query tanpa terlihat. Akibatnya halaman daftar pesanan yang tadinya cepat tiba-tiba butuh tiga detik setelah data mencapai ribuan baris. Masalahnya jarang ada di Eloquent, melainkan di cara memakainya.
Artikel ini menjelaskan Eloquent ORM di Laravel 11 dengan contoh studi kasus toko sederhana (pelanggan, pesanan, produk): cara kerja model, operasi CRUD, relasi, eager loading, accessor, scope, soft delete, serta batas di mana Query Builder atau SQL mentah lebih tepat.
1. Apa Itu ORM dan Bagaimana Eloquent Bekerja
ORM (Object-Relational Mapping) memetakan baris tabel menjadi objek PHP. Eloquent memakai pola Active Record: satu objek model mewakili satu baris, dan objek itu tahu cara menyimpan dirinya sendiri. Di balik layar, setiap pemanggilan method seperti where() atau get() diterjemahkan ke SQL lewat Query Builder, lalu dijalankan memakai PDO dengan parameter binding sehingga aman dari SQL injection selama Anda tidak menyisipkan input mentah ke whereRaw().
2. Model dan Konvensi
php artisan make:model Order -mf
Opsi -m membuat migration dan -f membuat factory. Berdasarkan konvensi, model Order dipetakan ke tabel orders, primary key id, dan kolom created_at/updated_at diisi otomatis.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Order extends Model
{
use SoftDeletes;
protected $fillable = ['customer_id', 'invoice_no', 'status', 'total', 'paid_at'];
protected function casts(): array
{
return [
'total' => 'decimal:2',
'paid_at' => 'datetime',
'meta' => 'array',
];
}
}
Di Laravel 11, casts ditulis sebagai method casts(). Cast datetime mengubah kolom menjadi objek Carbon, sehingga Anda bisa menulis $order->paid_at->format('d M Y'). Properti $fillable membatasi kolom yang boleh diisi massal, melindungi dari pengguna yang menyisipkan field seperti status=paid lewat form.
3. CRUD dengan Eloquent
// Read
$order = Order::findOrFail($id); // 404 otomatis jika tidak ada
$recent = Order::where('status', 'paid')
->latest()
->limit(20)
->get();
// Create
$order = Order::create([
'customer_id' => $customer->id,
'invoice_no' => 'INV-2026-0001',
'status' => 'pending',
'total' => 150000,
]);
// Update
$order->update(['status' => 'paid', 'paid_at' => now()]);
// Upsert sederhana
Customer::updateOrCreate(
['phone' => '628123456789'],
['name' => 'Budi']
);
// Delete (soft delete karena model memakai SoftDeletes)
$order->delete();
Perbedaan yang perlu diingat: get() mengembalikan Collection, first() mengembalikan satu model atau null, dan findOrFail() melempar exception yang otomatis menjadi halaman 404.
4. Relasi Antar Model
| Relasi | Contoh | Method |
|---|---|---|
| One to many | Satu pelanggan punya banyak pesanan | hasMany / belongsTo |
| One to one | Satu user punya satu profil | hasOne / belongsTo |
| Many to many | Pesanan berisi banyak produk, produk ada di banyak pesanan | belongsToMany + tabel pivot |
| Has many through | Pelanggan ke item pesanan lewat pesanan | hasManyThrough |
// Customer.php
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
// Order.php
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class, 'order_items')
->withPivot(['qty', 'price'])
->withTimestamps();
}
Kolom price di tabel pivot sengaja disimpan: harga produk bisa berubah, tetapi nilai pesanan lama harus tetap sesuai harga saat transaksi.
5. Masalah N+1 dan Eager Loading
Ini penyebab paling umum aplikasi Laravel melambat. Perhatikan kode berikut:
$orders = Order::latest()->take(50)->get();
foreach ($orders as $order) {
echo $order->customer->name; // 1 query per pesanan
}
Hasilnya 1 query untuk pesanan ditambah 50 query untuk pelanggan, total 51 query. Dengan eager loading jumlahnya turun menjadi 2:
$orders = Order::with(['customer:id,name', 'products'])
->withCount('products')
->latest()
->take(50)
->get();
Agar kesalahan ini ketahuan sejak development, aktifkan proteksi lazy loading di AppServiceProvider:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
Dengan ini, setiap akses relasi yang belum di-load akan melempar exception di lingkungan lokal. Pembahasan lebih dalam ada di optimasi performa query Eloquent.
6. Accessor, Mutator, dan Scope
Accessor dan mutator di Laravel 11 memakai kelas Attribute:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function invoiceNo(): Attribute
{
return Attribute::make(
set: fn (string $value) => strtoupper(trim($value)),
);
}
protected function totalFormatted(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->total, 0, ',', '.'),
);
}
Local scope membungkus kondisi yang sering dipakai agar tidak ditulis ulang:
public function scopePaid($query)
{
return $query->where('status', 'paid');
}
public function scopeBetween($query, $from, $to)
{
return $query->whereBetween('created_at', [$from, $to]);
}
// pemakaian
$omzet = Order::paid()->between(now()->startOfMonth(), now())->sum('total');
7. Mengolah Data Besar
Memanggil Order::all() pada tabel berisi ratusan ribu baris akan menghabiskan memori. Gunakan pemrosesan bertahap:
chunkById(500, fn ($rows) => ...)untuk memproses per 500 baris dengan aman walau ada update.lazy()untuk iterasi satu per satu dengan memori rendah.->select([...])untuk mengambil kolom yang dibutuhkan saja.paginate(20)di halaman daftar, janganget().
8. Kapan Tidak Memakai Eloquent
Eloquent unggul untuk CRUD dan logika per record. Untuk laporan agregat besar (misalnya omzet per kategori per bulan dari jutaan baris), Query Builder atau SQL dengan DB::select() sering lebih cepat dan lebih mudah dibaca, karena Anda tidak perlu membuat objek model untuk setiap baris. Mencampur keduanya dalam satu proyek adalah hal yang wajar.
9. Kesalahan Umum dan Solusinya
- MassAssignmentException: kolom belum ada di
$fillable. Tambahkan, jangan mematikan proteksi dengan$guarded = []di model yang menerima input pengguna. - "Call to a member function on null": relasi
belongsTokosong. Pakai$order->customer?->nameatauwithDefault(). - Data yang dihapus masih muncul atau hilang tak terduga: lupa bahwa SoftDeletes menyaring otomatis. Gunakan
withTrashed()bila perlu. - Total uang meleset: menyimpan uang sebagai float. Gunakan kolom
DECIMALdan castdecimal:2, atau simpan dalam satuan terkecil sebagai integer.
Checklist Model yang Sehat
$fillableberisi hanya kolom yang memang diisi dari form.- Kolom tanggal, boolean, JSON, dan uang punya cast yang tepat.
- Semua relasi diberi return type (
HasMany,BelongsTo). - Halaman daftar memakai
with()danpaginate(). preventLazyLoadingaktif di lingkungan development.- Kolom yang sering difilter (
status,customer_id) punya index di migration.
Sebelum membuat model, rancang tabelnya dulu; panduan merancang ERD sederhana membantu menentukan relasi yang tepat, dan migrasi database yang aman membahas cara mengubah struktur tabel tanpa kehilangan data.