Model::with

Eager loads the named relationships, replacing the N+1 query pattern with a fixed query count.

Signature
public function with(array|string $relations, \Closure|string|null $callback = null): $this

Parameters

ParameterTypeDescription
$relationsarray|stringA relationship name, an array of names, or an array keyed by name with Closure values for constraints. Multiple names may also be passed as separate variadic arguments. Dot syntax nests.
$callbackoptional\Closure|string|null= nullA constraint applied when $relations is a single relationship name given as a string.

Return value

$thisThe same builder instance, so calls can be chained. The relationships are merged into the builder's eager-load list and resolved when the query is executed.

Errors

Throws Illuminate\Database\Eloquent\RelationNotFoundException when a named relationship is not defined on the model.

with() tells Eloquent which relationships to fetch alongside the parent models. Instead of one query per parent, Eloquent runs a single extra query per relationship using a whereIn over the collected keys, then matches the results back onto the models in PHP.

The N+1 problem

Accessing $book->author inside a loop lazy loads: 25 books produce 26 queries. Book::with('author')->get() produces 2, regardless of how many books there are. This is the single highest-value change most Laravel pages get.

Forms

  • Several relationships: with(['author', 'publisher']).
  • Nested, dot syntax: with('author.contacts').
  • Constrained: with(['posts' => fn ($q) => $q->where('title', 'like', 'code%')]).
  • After retrieval: $books->load('author'), for when the decision is conditional.

What trips people up

Selecting specific columns inside a constraint must include the joining key. Omit user_id from a hasMany eager load and every parent gets an empty relation, because Eloquent has nothing to match on.

A constrained eager load does not filter the parents — a book with no matching posts still comes back, with an empty collection. That is whereHas()'s job.

Eager loading is not free: it loads every related row into memory. For counts, withCount() is far cheaper.

Example

Example · php
<?php

use App\Models\Book;

// Lazy loading: 1 query for books + 1 per book = 26 queries.
$books = Book::all();

foreach ($books as $book) {
    echo $book->author->name, "\n";
}

// Eager loading: 2 queries, whatever the row count.
$books = Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name, "\n";
}

// Several relationships, and a nested one.
$books = Book::with(['author', 'publisher'])->get();
$books = Book::with('author.contacts')->get();

echo $books->count(), "\n";

When to use it

  • Render a list of posts with their author names without issuing a query per row.
  • Preload a nested chain such as orders, their items, and each item's product for an invoice view.
  • Load only the current month's transactions for each account in a dashboard.

Frequently asked questions

How does with() fix the N+1 query problem?

It collects the parent keys after the first query and fetches all related rows in one additional query per relationship using a whereIn, then matches them onto the models in PHP. So 25 books with their authors go from 26 queries to 2, and the count stays at 2 no matter how many books there are. Nested relationships add one query per level, not one per row.

Why is my eager loaded relationship empty when I select specific columns?

Because the column list omitted the key Eloquent uses to match related rows back to their parents. For a hasMany, include the foreign key — with('posts:id,user_id,title') rather than with('posts:id,title'). For a belongsTo, include the related model's primary key. Nothing errors; every parent simply receives an empty relation.

What is the difference between with() and load()?

with() is set on the builder before the query runs, so relationships come back with the initial fetch; load() is called on models you already have and issues the batched queries at that moment. Both avoid N+1 equally — use load() when the decision depends on something you only learn after retrieving the parents, such as a request flag or a policy check.

More examples

Constraining an eager load

The loop prints 0 for users whose posts all failed the filter — the constraint shapes the relation, never the parent list. The second query pairs whereHas() with the same closure so only genuine authors are returned and their posts are still filtered.

Example · php
<?php

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;

$users = User::with(['posts' => function (Builder $query) {
    $query->where('title', 'like', 'code%')
        ->orderByDesc('created_at')
        ->limit(5);
}])->get();

// Users with no matching posts are still returned - with an empty relation.
foreach ($users as $user) {
    echo $user->name, ': ', $user->posts->count(), "\n";
}

// To exclude those users, filter the parents as well.
$authors = User::whereHas('posts', fn (Builder $q) => $q->where('title', 'like', 'code%'))
    ->with(['posts' => fn (Builder $q) => $q->where('title', 'like', 'code%')])
    ->get();

echo $authors->count(), "\n";

The column-selection trap

The first loop prints zero for every user and no error is raised — Eloquent fetched the posts but had no user_id to key them by. The fix is one column. For belongsTo the local foreign key is already on the parent, so only the related primary key is required.

Example · php
<?php

use App\Models\User;

// BROKEN: posts.user_id is missing, so nothing matches back.
$users = User::with(['posts:id,title'])->get();

foreach ($users as $user) {
    echo $user->posts->count(), "\n";   // 0 for everyone
}

// FIXED: include the foreign key Eloquent matches on.
$users = User::with(['posts:id,user_id,title'])->get();

foreach ($users as $user) {
    echo $user->posts->count(), "\n";   // real counts
}

// belongsTo needs the owner key on the related table instead.
$posts = Post::with('author:id,name')->get();

Deciding after the fact, and avoiding over-loading

load() issues the same batched queries with() would have, so a conditional detail view costs nothing when the flag is off. The withCount() line shows the cheaper alternative when only a total is needed, and preventLazyLoading turns any remaining N+1 into a loud exception outside production.

Example · php
<?php

use App\Models\Book;
use Illuminate\Database\Eloquent\Builder;

$books = Book::all();

// Lazy eager loading: same batching, decided later.
if (request()->boolean('detailed')) {
    $books->load('author', 'publisher');
}

echo $books->count(), "\n";

// Only need a number? Do not load the rows.
$books = Book::withCount('reviews')->get();
echo $books->first()->reviews_count, "\n";

// Catch accidental lazy loads in development.
Model::preventLazyLoading(! app()->isProduction());

Discussion

  • Be the first to comment on this lesson.