Eager Loading, N+1 & Query Scope
Menghindari masalah N+1 dengan with(), withCount, dan membuat query scope yang bisa dipakai ulang (#[Scope]).
Masalah N+1
Perhatikan kode yang tampak tak bersalah ini:
$produks = Produk::all(); // 1 query
foreach ($produks as $produk) {
echo $produk->kategori->nama; // 1 query PER produk!
}
Dengan 100 produk, aplikasi menjalankan 1 + 100 = 101 query. Inilah N+1 query problem: penyebab klasik aplikasi lambat. Relasi dimuat "malas" (lazy loading) saat pertama diakses, satu per satu.
Solusi: eager loading dengan with()
$produks = Produk::with('kategori')->get(); // hanya 2 query, berapa pun jumlah produknya
foreach ($produks as $produk) {
echo $produk->kategori->nama; // sudah dimuat, tanpa query tambahan
}
Laravel menjalankan satu query untuk produk dan satu query WHERE id IN (...) untuk semua kategori terkait, lalu memasangkannya di memori.
Produk::with(['kategori', 'tags'])->get(); // beberapa relasi
Produk::with('kategori:id,nama')->get(); // hanya kolom tertentu
Produk::with('kategori.negara')->get(); // relasi bersarang
$produk->load('tags'); // muat belakangan pada model yang sudah ada
Kategori::withCount('produks')->get(); // kolom produks_count tanpa memuat produknya
Produk::withSum('itemPesanan', 'qty')->get(); // agregat relasi
Mencegah N+1 sejak dini
Di AppServiceProvider::boot():
use Illuminate\Database\Eloquent\Model;
Model::preventLazyLoading(! app()->isProduction()); // lazy loading → exception saat development
Dengan ini N+1 langsung ketahuan saat kamu mengembangkan. Alat bantu lain: Laravel Debugbar, Telescope, atau paket laravel/nightwatch untuk memantau query.
Query scope: filter yang bisa dipakai ulang
Kondisi yang sering diulang (where('aktif', true)->where('stok', '>', 0)) dibungkus menjadi scope. Di Laravel 13 cukup beri atribut #[Scope] pada method protected:
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
class Produk extends Model
{
#[Scope]
protected function tersedia(Builder $query): void
{
$query->where('aktif', true)->where('stok', '>', 0);
}
#[Scope]
protected function dalamKategori(Builder $query, int $kategoriId): void // scope berparameter
{
$query->where('kategori_id', $kategoriId);
}
}
Produk::tersedia()->get();
Produk::tersedia()->dalamKategori(3)->latest()->paginate(10); // bisa dirantai
Cara lama dengan awalan scope (scopeTersedia) tetap berfungsi. Method scope mengembalikan void dan memodifikasi $query yang diterima.
Global scope
Filter yang selalu diterapkan pada semua query model (mis. hanya data tenant aktif): buat class Scope dan pasang dengan atribut #[ScopedBy([ScopeAktif::class])] pada model. Soft delete sendiri adalah global scope.
Menggabungkan semuanya
public function index()
{
$produks = Produk::with('kategori:id,nama')
->withCount('tags')
->tersedia()
->latest()
->paginate(12);
return view('produk.index', compact('produks'));
}
Ringkasan
- Melihat relasi di loop? Pakai
with()di query awal. - Butuh jumlah saja?
withCount(). - Kondisi berulang? Jadikan scope.