LaravelEloquentRelasiMenengah4 mnt baca

Query Lewat Relasi: whereHas dan withCount

Filter dan hitung berdasarkan relasi tanpa N+1.

Masalah yang Dipecahkan Dua Method Ini

Dua kebutuhan muncul terus saat bekerja dengan relasi. Pertama: "tampilkan parent yang child-nya memenuhi syarat", misalnya user yang punya post viral. Kedua: "tampilkan jumlah child tanpa memuat semuanya", misalnya jumlah komentar tiap post di halaman daftar. whereHas dan withCount menjawab keduanya sebagai query SQL yang efisien, tanpa N+1 dan tanpa memuat data yang tidak perlu.

whereHas: Filter Parent Berdasarkan Child

php
// app/Http/Controllers/UserController.php
$users = User::whereHas('posts', function ($query) {
    $query->where('views', '>', 1000);
})->get();

Dibaca: ambil user yang MEMILIKI post dengan views di atas 1000. Di balik layar Laravel membuat EXISTS subquery, jadi filternya terjadi di database, bukan di PHP. Butuh kebalikannya? whereDoesntHave() mengambil parent yang TIDAK punya child memenuhi syarat, misalnya user yang belum pernah posting.

php
// app/Http/Controllers/UserController.php
$silent = User::whereDoesntHave('posts')->get(); // user tanpa post sama sekali

withCount: Hitung Tanpa Memuat

php
// app/Http/Controllers/PostController.php
$posts = Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->title . ' (' . $post->comments_count . ' komentar)';
}

Satu query dengan subquery COUNT, hasilnya ditempel sebagai atribut virtual comments_count. Bandingkan dengan $post->comments->count() di dalam loop: itu memuat SEMUA komentar tiap post ke memori hanya untuk dihitung. Untuk daftar 50 post dengan ratusan komentar, bedanya dramatis.

Kombinasi: Hitung Subset Lalu Urutkan

php
// app/Http/Controllers/UserController.php
$topAuthors = User::withCount(['posts' => function ($query) {
        $query->where('published_at', '<=', now());
    }])
    ->orderByDesc('posts_count')
    ->take(5)
    ->get();

withCount menerima array dengan closure untuk menghitung subset saja: di sini hanya post yang sudah published. Hasil agregatnya bisa langsung dipakai di orderBy karena posts_count diperlakukan sebagai kolom biasa dalam query.

Jebakan umum: memanggil withCount lalu mengakses $post->comments (relasinya) dan berharap datanya sudah di-load. withCount TIDAK memuat relasi, ia hanya menghitung. Kalau butuh datanya juga, tambahkan with('comments') secara terpisah.

Catatan teknis: withCount punya saudara kandung: withSum, withAvg, withMax, dan withMin. Contoh: Post::withSum('comments', 'likes')->get() menghasilkan atribut comments_sum_likes. Pola penamaannya selalu {relasi}{fungsi}{kolom}. Ini cara termurah menampilkan statistik agregat di halaman daftar tanpa query tambahan per baris, dan semuanya dieksekusi sebagai subquery SQL yang efisien.

whereHas Bersarang dan orWhereHas

Kondisi whereHas bisa menembus dua tingkat relasi dengan dot notation:

php
// app/Http/Controllers/CountryController.php
$countries = Country::whereHas('users.posts', function ($query) {
    $query->where('views', '>', 10000);
})->get(); // negara yang punya post viral lewat user-nya

Dan bisa digabung dengan OR:

php
// app/Http/Controllers/PostController.php
$posts = Post::where('featured', true)
    ->orWhereHas('comments', function ($query) {
        $query->where('likes', '>', 50);
    })->get();

Hati-hati mencampur where dan orWhereHas tanpa grouping: OR berlaku terhadap seluruh kondisi sebelumnya, bukan hanya kondisi terakhir. Untuk logika kompleks, bungkus dalam closure where(function ($q) { ... }) agar urutan evaluasinya jelas dan tidak mengejutkan.

Tantangan

Top Penulis

Via tinker: tampilkan 5 user dengan post published terbanyak (pakai withCount + orderByDesc). Tampilkan nama dan jumlahnya.