LaravelEloquentRelasiMenengah4 mnt baca

Relasi Bertingkat: hasManyThrough

Akses data 'cucu' langsung: negara punya banyak post lewat user.

Masalah: Data "Cucu"

Strukturnya Country -> User -> Post. Pertanyaan "tampilkan semua post dari negara X" berarti melompati satu tingkat relasi. Tanpa bantuan khusus, kamu harus mengambil semua user dari negara itu, lalu loop untuk mengambil post tiap user satu per satu. Itu N+1 yang menyamar, dan makin lambat seiring jumlah user bertambah.

Solusi: hasManyThrough

php
// app/Models/Country.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(Post::class, User::class);
    }
}
php
// app/Http/Controllers/CountryController.php
$country = Country::find(1);

foreach ($country->posts as $post) {
    echo $post->title; // semua post dari semua user negara ini
}

$totalViews = $country->posts()->sum('views'); // agregasi lintas dua tingkat

Dibaca: Country has many Post THROUGH User. Laravel otomatis men-join tabel users di tengah memakai konvensi foreign key (users.country_id dan posts.user_id). Kekuatan sebenarnya ada di agregasi: total views, jumlah post, rata-rata views per negara, semua bisa dihitung langsung dari $country->posts() tanpa memuat satu pun model ke memori.

Alternatif: Nested Eager Loading

php
// app/Http/Controllers/CountryController.php
$users = User::with('posts')->where('country_id', 1)->get();

foreach ($users as $user) {
    echo $user->name . ' menulis:';
    foreach ($user->posts as $post) {
        echo ' - ' . $post->title;
    }
}

Kalau tampilan butuh struktur hierarki (negara, lalu tiap user, lalu post masing-masing), nested with() lebih natural daripada meratakan semuanya menjadi satu daftar. Aturan memilihnya sederhana: butuh daftar datar atau agregasi lintas tingkat? Pakai hasManyThrough. Butuh struktur bertingkat untuk ditampilkan ke user? Pakai with() bersarang.

Jebakan umum: memakai hasManyThrough untuk rantai yang tidak mengikuti konvensi key lalu bingung kenapa hasilnya kosong. Kalau nama foreign key-mu custom, hasManyThrough menerima argumen tambahan untuk menyebutkannya eksplisit: hasManyThrough(Post::class, User::class, 'country_id', 'user_id'). Cek dulu konvensi sebelum menyalahkan relasinya.

Catatan teknis: Ada juga hasOneThrough untuk versi singular (contoh: Mechanic has one Car through Owner, untuk mengambil satu mobil milik satu mekanik lewat pemiliknya). Untuk rantai yang lebih panjang dari dua tingkat, tidak ada hasManyThroughThrough; solusinya adalah nested with() atau query builder dengan join manual. Pilih alat sesuai kedalaman masalah, jangan memaksakan satu relasi untuk semua kasus.

Agregasi Lintas Tingkat untuk Dashboard

Kekuatan hasManyThrough paling terasa di halaman dashboard. Contoh nyata: leaderboard negara berdasarkan total views post:

php
// app/Http/Controllers/DashboardController.php
$top = Country::withSum('posts', 'views')
    ->orderByDesc('posts_sum_views')
    ->take(10)
    ->get();

withSum bekerja di atas relasi hasManyThrough sama seperti relasi biasa. Satu query menghasilkan 10 negara dengan total views tertinggi. Tanpa relasi ini kamu harus join manual atau loop bertingkat yang lambat. Pola yang sama berlaku untuk withCount('posts') (jumlah post per negara) dan withAvg, plus whereHas untuk filter: Country::whereHas('posts', fn ($q) => $q->where('views', '>', 1000))->get().

Tantangan

Post per Negara

Asumsikan Country hasMany User dan User hasMany Post. Definisikan hasManyThrough di Country. Via tinker: hitung total post untuk 1 negara.