Eager Loading, N+1 & Query Scope

Menghindari masalah N+1 dengan with(), withCount, dan membuat query scope yang bisa dipakai ulang (#[Scope]).

Part 13 dari 26 ± 40 menit Gratis

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.

Praktikkan langsung, gratis

Tulis dan jalankan kode langsung di browser, dapatkan penilaian otomatis. Daftar gratis kapan saja untuk menyimpan progres dan meraih sertifikat.

Coba latihan sekarang, tanpa daftar
Bagikan: WhatsApp Facebook LinkedIn X