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
// 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.
// app/Http/Controllers/UserController.php
$silent = User::whereDoesntHave('posts')->get(); // user tanpa post sama sekaliwithCount: Hitung Tanpa Memuat
// 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
// 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:
// app/Http/Controllers/CountryController.php
$countries = Country::whereHas('users.posts', function ($query) {
$query->where('views', '>', 10000);
})->get(); // negara yang punya post viral lewat user-nyaDan bisa digabung dengan OR:
// 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.