Relasi di Eloquent bukan hanya jalan untuk membaca data terkait — relasi juga menjadi pintu masuk query. Setiap method relasi seperti $post->comments() mengembalikan query builder yang sudah otomatis dibatasi pada induknya, sehingga kita bisa merantai where() di atasnya tanpa menulis foreign key manual. Di sisi lain, sering kali arahnya terbalik: kita ingin mencari post berdasarkan isi relasinya — “post yang punya komentar approved” — dan untuk itu Eloquent menyediakan keluarga whereHas() . Chapter ini mengulas kedua arah tersebut dengan query nyata di atas data enam post dan sembilan komentar yang sama, dan setiap SQL penting kita periksa lewat toSql().

Query Lewat Method Relasi #

Memanggil $post->comments (tanpa kurung) mengembalikan koleksi; memanggil $post->comments() (dengan kurung) mengembalikan query builder milik relasi. Beda satu tanda kurung, hasilnya jauh berbeda:

php
use App\Models\Post;

$post = Post::find(1);

$approved = $post->comments()->where('approved', true)->orderBy('id')->get();

foreach ($approved as $comment) {
    echo $comment->isi, "\n";
}
echo 'jumlah total: ', $post->comments()->count(), "\n";
echo 'jumlah approved: ', $post->comments()->where('approved', true)->count(), "\n";

Output yang Dihasilkan:

text
Penjelasannya jelas, terima kasih!
Baru mulai belajar, sangat membantu.
jumlah total: 3
jumlah approved: 2

Query builder ini mewarisi constraint relasi secara otomatis. SQL untuk variabel $approved berbunyi select * from "comments" where "comments"."post_id" = ? and "comments"."post_id" is not null and "approved" = ? — klausa post_id = ? dan pemeriksaan is not null disuntikkan oleh relasi hasMany, bukan oleh kode kita. Method aggregate juga tersedia langsung: count() pada contoh di atas cukup memanggil select count(*) tanpa memuat satu pun model.

Memfilter Induk: whereHas() #

Arah kedua adalah mencari model induk berdasarkan kondisi pada relasinya. whereHas() membungkus kondisi relasi sebagai subquery exists:

php
$posts = Post::whereHas('comments', fn ($query) => $query->where('approved', true))
    ->orderBy('id')->get();

foreach ($posts as $post) {
    echo $post->judul, "\n";
}
echo "SQL: ", Post::whereHas('comments', fn ($q) => $q->where('approved', true))->toSql(), "\n";

Output yang Dihasilkan:

text
Belajar Relasi 01
Belajar Relasi 02
Belajar Relasi 04
Belajar Relasi 05
SQL: select * from "posts" where exists (select * from "comments" where "posts"."id" = "comments"."post_id" and "approved" = ?)

SQL-nya menunjukkan cara kerjanya: setiap baris posts diuji dengan exists (select ... from comments ...) — ada tidaknya minimal satu komentar approved menentukan post lolos atau tidak. Perhatikan pula bahwa whereHas tidak memuat relasinya; hasilnya tetap koleksi Post bersih. Untuk versi ringkas tanpa closure, whereRelation() meneruskan argumen sebagai kondisi:

php
$posts = Post::whereRelation('comments', 'approved', true)->orderBy('id')->get();

Output yang Dihasilkan (kolom judul post yang lolos):

text
Belajar Relasi 01
Belajar Relasi 02
Belajar Relasi 04
Belajar Relasi 05

Bila di saat yang sama kita juga butuh relasinya termuat, gabungkan filter dan eager loading sekaligus dengan withWhereHas():

php
$posts = Post::withWhereHas('comments', fn ($query) => $query->where('approved', true))
    ->orderBy('id')->get();

foreach ($posts as $post) {
    echo $post->judul, ': ', $post->comments->count(), " komentar approved\n";
}

Output yang Dihasilkan:

text
Belajar Relasi 01: 2 komentar approved
Belajar Relasi 02: 1 komentar approved
Belajar Relasi 04: 2 komentar approved
Belajar Relasi 05: 1 komentar approved

Satu panggilan menghasilkan query exists untuk filter dan query where ... in untuk memuat relasi — padanan whereHas() plus with() pada relasi yang sama.

has(), doesntHave(), dan orWhereHas() #

Tanpa kondisi tambahan, has() cukup menguji keberadaan relasi. Operator pembanding dan jumlah ikut diterima:

php
echo "3 komentar atau lebih:\n";
foreach (Post::has('comments', '>=', 3)->orderBy('id')->get() as $post) {
    echo '  ', $post->judul, "\n";
}
echo "tanpa komentar:\n";
foreach (Post::doesntHave('comments')->orderBy('id')->get() as $post) {
    echo '  ', $post->judul, "\n";
}
echo "SQL: ", Post::has('comments', '>=', 3)->toSql(), "\n";

Output yang Dihasilkan:

text
3 komentar atau lebih:
  Belajar Relasi 01
  Belajar Relasi 04
tanpa komentar:
  Belajar Relasi 03
  Belajar Relasi 06
SQL: select * from "posts" where (select count(*) from "comments" where "posts"."id" = "comments"."post_id") >= 3

Berbeda dengan whereHas() yang memakai exists, has() dengan operator hitungan menghasilkan subquery count(*) yang dibandingkan — perhatikan (select count(*) from "comments" ...) >= 3 pada SQL di atas. doesntHave() adalah negasinya, berguna untuk mencari data yatim relasi seperti post yang belum dikomentari. Untuk kondisi gabungan, orWhereHas() menambahkan cabang OR:

php
$posts = Post::where('id', '<=', 2)
    ->orWhereHas('comments', fn ($query) => $query->where('user_id', 3))
    ->orderBy('id')->get();

foreach ($posts as $post) {
    echo $post->judul, "\n";
}

Output yang Dihasilkan:

text
Belajar Relasi 01
Belajar Relasi 02
Belajar Relasi 04

Post 1 dan 2 lolos lewat kondisi id <= 2, sedangkan post 4 lolos karena Anton (user_id 3) berkomentar di sana. Pola where ... orWhereHas ... seperti ini adalah bentuk sederhana pencarian gabungan atribut dan relasi.

Mengurutkan Berdasarkan Jumlah Relasi #

Kombinasi withCount() dan orderBy() mengurutkan induk berdasarkan banyaknya relasi — misalnya post paling ramai komentar:

php
$posts = Post::withCount('comments')
    ->orderByDesc('comments_count')
    ->orderBy('id')
    ->get();

foreach ($posts as $post) {
    echo $post->judul, ': ', $post->comments_count, "\n";
}

Output yang Dihasilkan:

text
Belajar Relasi 01: 3
Belajar Relasi 04: 3
Belajar Relasi 02: 2
Belajar Relasi 05: 1
Belajar Relasi 03: 0
Belajar Relasi 06: 0

Kolom virtual comments_count dari withCount() diproduksi sebagai subquery di SQL, sehingga orderBy bisa langsung memakainya. Tie-breaker orderBy('id') kita tambahkan agar urutan post dengan jumlah sama tetap deterministik — post 1 dan 4 sama-sama punya tiga komentar.

Dengan tiga pintu ini — query lewat method relasi, whereHas() untuk menembus relasi, dan withCount() untuk mengurutkan — hampir semua kebutuhan penyaringan berbasis relasi sudah tercakup. Pada chapter Pivot Table berikutnya kita masuk ke dalam tabel perantara role_user: bagaimana membaca dan mengelola kolom-kolom ekstra di dalamnya.