LaravelEloquentPerformaMenengah4 mnt baca

Masalah N+1: Musuh Performa Tersembunyi

Kenali, ukur, dan basmi query N+1.

Kenapa N+1 Itu Berbahaya

N+1 adalah pola query di mana 1 query awal untuk mengambil daftar data diikuti N query tambahan, satu per baris, setiap kali kamu menyentuh relasi yang belum di-load. Contoh klasiknya seperti ini:

php
// app/Http/Controllers/PostController.php
$posts = Post::all(); // 1 query: SELECT * FROM posts

foreach ($posts as $post) {
    echo $post->user->name; // 1 query PER post: SELECT * FROM users WHERE id = ?
}
// 100 post = 101 query ke database

Di laptop development dengan 10 baris data, ini tidak terasa sama sekali. Halaman tetap terbuka dalam 50ms dan kamu merasa semuanya baik-baik saja. Masalahnya muncul di production: 10.000 baris berarti 10.001 query, koneksi database jenuh, dan halaman yang tadinya 50ms berubah menjadi 20 detik. Ini bug performa nomor satu di aplikasi Laravel karena ia tidak terlihat sampai datanya besar.

Melihat Masalahnya dengan Mata Sendiri

Jangan menebak, ukur. Aktifkan query log dan hitung sendiri:

php
// routes/web.php (sementara, untuk debugging)
use Illuminate\Support\Facades\DB;

Route::get('/cek-query', function () {
    DB::enableQueryLog();

    $posts = Post::take(20)->get();
    foreach ($posts as $post) {
        echo $post->user->name;
    }

    dd(count(DB::getQueryLog())); // 21 kalau kena N+1
});

Di project nyata, install Laravel Debugbar (composer require barryvdh/laravel-debugbar --dev) supaya setiap halaman menampilkan jumlah query di toolbar. Kalau satu halaman sederhana menembak 200+ query, hampir pasti ada N+1 di dalamnya.

Solusi 1: Eager Loading

php
// 2 query total, tidak peduli ada berapa post
$posts = Post::with('user')->latest()->get();

foreach ($posts as $post) {
    echo $post->user->name; // tanpa query tambahan
}

Kamu juga bisa membatasi kolom dan menambah kondisi di eager load:

php
$posts = Post::with(['user:id,name', 'comments' => function ($query) {
    $query->where('approved', true)->latest()->take(5);
}])->get();

Solusi 2: withCount untuk Agregat

Jebakan klasik lain: menampilkan jumlah komentar per post di dalam loop.

php
// SALAH: 1 query COUNT per post
foreach ($posts as $post) {
    echo $post->comments()->count();
}

// BENAR: 1 query agregat untuk semua post
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
    echo $post->comments_count; // tanpa query tambahan
}

Jebakan yang Sering Terjadi

Pertama, relasi bertingkat yang terlupa. Kamu eager load comments, tapi di Blade mengakses $comment->user->name. Itu N+1 baru di level komentar. Solusinya: Post::with('comments.user')->get().

Kedua, over-eager-loading. Memasang with() untuk lima relasi padahal view cuma butuh satu kolom dari satu relasi. Eager load tetap mengeksekusi query, jadi load hanya yang dipakai.

Ketiga, mengakses relasi di dalam API Resource tanpa whenLoaded(). Resource dipanggil per model, jadi $this->user->name di dalam toArray() memicu lazy load per item kalau relasinya tidak di-eager-load. Pakai $this->whenLoaded('user') agar aman.

Pagar Pengaman Otomatis

Laravel bisa melempar exception setiap kali lazy loading terjadi, sehingga N+1 ketahuan sejak development:

php
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

Dengan ini, setiap akses relasi yang belum di-eager-load langsung error di local. Kamu dipaksa memperbaiki pola query sebelum kode sampai ke production. Matikan di production (parameter false) karena exception ini bukan untuk user.

Catatan teknis: N+1 bukan sekadar soal with(). Ini soal kebiasaan membaca alur data: setiap kali kamu menulis loop yang menyentuh relasi, berhenti sejenak dan tanya apakah relasi itu sudah tersedia di memori. Kombinasi Debugbar saat development dan preventLazyLoading() sebagai pagar akan menangkap 90% kasus sebelum menjadi masalah production.

Tantangan

Buru N+1

Buat route yang menampilkan 20 post beserta nama penulisnya TANPA with(). Hitung query-nya dengan getQueryLog(). Lalu tambahkan with('user') dan hitung lagi. Laporkan penurunannya.

Kuis Bab

Uji pemahamanmu: Bab 6: Relasi Eloquent

Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".

1.Apa beda $user->posts dan $user->posts()?

2.Method apa yang paling aman untuk update tag dari form checkbox?

3.Bagaimana mengakses kolom tambahan pivot?

4.Apa itu masalah N+1?

5.Kapan memakai withCount() dibanding with()?