Relationships and eager loading

Laravel · Practical · 15 min

Relationships are the heart of real data models, and N+1 queries are the most common Laravel performance bug.

Relationships are methods on models that return relationship objects:

  • hasMany/belongsTo: a User has many Posts (posts.user_id), and each Post belongs to a User.
  • hasOne, belongsToMany (many-to-many through a pivot table such as post_tag, with attach, detach and sync), hasManyThrough, and polymorphic morphMany/morphTo.

Access a relationship as a property to get the results ($user->posts is a Collection, loaded once and cached on the model) or as a method to keep querying: $user->posts()->where('published', true)->count(). Create related rows through the relation: $user->posts()->create([...]) fills user_id for you.

The N+1 problem: looping over 50 posts and printing $post->author->name runs 1 query for posts plus 50 for authors. Fix it with eager loading:

$posts = Post::with(['author', 'tags'])->latest()->get();   // 3 queries total
  • with('comments.author') loads nested relations; constrain with with(['comments' => fn ($q) => $q->latest()]).
  • load('author') eager-loads onto models you already have.
  • withCount('comments') adds a comments_count attribute without loading rows; whereHas('comments', ...) filters by related rows.

Add Model::preventLazyLoading(! app()->isProduction()); in AppServiceProvider::boot so lazy loading throws during development and N+1 bugs surface immediately.

Example

<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\{BelongsTo, BelongsToMany, HasMany};

class Post extends Model
{
    public function author(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); }
    public function comments(): HasMany { return $this->hasMany(Comment::class); }
    public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class)->withTimestamps(); }
}

// N+1: 1 query + 1 per post
foreach (Post::all() as $post) echo $post->author->name;

// Eager loaded: 3 queries no matter how many posts
$posts = Post::with(['author', 'tags'])
    ->withCount('comments')                         // adds comments_count
    ->whereHas('tags', fn ($q) => $q->where('name', 'laravel'))
    ->latest()->get();
foreach ($posts as $p) echo "{$p->title} by {$p->author->name} ({$p->comments_count})";

$post->tags()->sync([1, 3]);                        // pivot rows now exactly 1 and 3
$user->posts()->create(['title' => 'Hi']);          // user_id set automatically

Remember

  • Relationship as property = loaded results; as method = query builder
  • with() eager-loads relations in one query each, fixing N+1
  • preventLazyLoading in development surfaces N+1 bugs early

Watch out for

  • Calling $post->comments()->count() inside a loop; use withCount on the parent query.

Check yourself

  1. Listing 100 posts with each author's name, how many queries does Post::with('author')->get() need?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: laravel.com/docs/12.x/eloquent-relationships