$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[October 17, 2024] Eloquent ORM

Eloquent Has Many Through: Distant Relations

Eloquent Has Many Through: Distant Relations

────────────────────────────────────────────────────────

Eloquent Has Many Through: Distant Relations

Tip: hasManyThrough() Basics

class Country extends Model
{
    public function posts(): HasManyThrough
    {
        return $this->hasManyThrough(Post::class, User::class);
    }
}

Country → Users → Posts. Get all posts by users in a country.

Gotcha: Table Name Inference

Laravel infers table names from model names. If your tables don't follow conventions, specify them:

return $this->hasManyThrough(
    Post::class, User::class,
    'country_id',     // Foreign key on users table
    'user_id',        // Foreign key on posts table
    'id',             // Local key on countries table
    'id'              // Local key on users table
);

Tip: hasManyThrough() with Constraints

$country->posts()->where('published', true)->get();

Scopes work on through relations.

Gotcha: No Eager Loading Optimization

hasManyThrough() still generates a JOIN. For large datasets, consider a denormalized approach.

Tip: Many-to-Many Through

No built-in support. Use a package or raw query for complex multi-level many-to-many.

Gotcha: hasThrough() Doesn't Exist

Use whereHas() on the through relation instead.

Tip: Use cursor() for Memory-Neutral Iteration

When exporting 100K rows, get() loads everything into memory. cursor() uses yield and keeps memory flat regardless of row count. Perfect for artisan commands.

Tip: Pivot Data Is Read-Only by Default

Many-to-many pivot columns are read-only unless you define custom pivot models. Use ->withPivot('expires_at') to make them accessible, and newPivot() to make them writable.

Gotcha: withCount() Adds a Subquery

withCount('comments') runs a correlated subquery on every row. On large tables, this can be slower than a separate query. Profile before relying on it.

Senior Insight

The hasManyThrough relationship is powerful but often the wrong tool. I've seen it used to create three-table joins that would be clearer as explicit query builder joins. The issue: hasManyThrough hides the intermediate table, making the generated SQL non-obvious. When debugging slow queries, developers overlook the intermediate table because it's invisible in the relationship definition. I use hasManyThrough only for simple reporting queries, never for critical data operations.

Source: Laravel Docs (https://laravel.com/docs/eloquent), Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/eloquent)

────────────────────────────────────────────────────────
<-- back to posts