laravelmastery
SnippetsArticlesSeriesNews

Eloquent, properly · Lesson 3 of 3

Eloquent, properly · Lesson 3

Eager loading on purpose

Query it safely and fastEloquentQueries

forTeam() already limited the list to one team. Lazy loading still feels fine on a page of twenty invoices, right up to the point each one asks for its team and its lines. That is one query for the list, then one per row, per relationship. Name the relationships this response will touch, and leave the rest for the response that needs them.

Video

01 Load the graph you already know

with() is the list of relationships this page will touch. Name the nested ones. Do not load the whole graph because a later view might ask. A path that never reads lines should not pay for lines. forTeam() is the local scope from the previous lessons, so the list is already one team's invoices before the eager load runs.

PHP
$invoices = Invoice::query()
    ->forTeam($team)
    ->with(['team', 'lines'])
    ->latest('issued_on')
    ->paginate(20);

In the view, $invoice->team and $invoice->lines are already there. Touching a relationship you did not list sends another query. Nested data uses the dot. with('lines.tags') loads the tags from the many-to-many lesson when the page actually prints them. Loading tags on a page that only prints descriptions is the unused query again.

Constrain an eager load when the page only wants the lines that matter. A where() inside with() applies to each invoice's lines. The closure receives the lines query, so orderBy here sorts each invoice's lines and does not sort the invoices. That order belongs in this closure, not as a default on the relationship, for the reason the one-to-many lesson gave: a default order is paid by every caller.

PHP
$invoices = Invoice::query()
    ->with(['lines' => function ($query): void {
        $query->where('amount', '>', 0)->orderBy('description');
    }])
    ->get();

limit() inside an eager load is per invoice from Laravel 11 on. Eloquent numbers the lines and keeps five for each parent. MySQL 8 does that with row_number(). Older MySQL uses a different query and still keeps five per invoice. A limit() on the lines query itself, outside with(), is the one that caps the whole table. Most invoices then look empty.

02 loadMissing when you already hold the invoice

The list uses with() because you are building the query. The show page often receives the invoice from route model binding, which has already run. with() on a query you do not control cannot help. loadMissing() loads the relationships that are not already on the instance, and it skips the ones a caller loaded for you.

PHP
public function show(Team $team, Invoice $invoice)
{
    $invoice->loadMissing(['team', 'lines']);

    return view('invoices.show', [
        'invoice' => $invoice,
    ]);
}

load() always queries, even when lines is already present. A view composer that calls load('lines') after the controller did will run the lines query twice. loadMissing('lines') is the call that is safe to repeat. Use load() when you mean to replace the collection, for example after you have created a line and the loaded set is stale. Use loadMissing() when you mean "make sure this view can read it".

The constraint closure works on loadMissing() the same way it works on with(). A show page that should hide zero-amount lines passes the same where. A show page that wants five lines passes the same limit(). The index has its own with() list. The next lesson turns a forgotten relationship into an exception outside production. Add the name on the action that rendered.

Recap

You can load the relationships an invoice page will touch, and use loadMissing() when the invoice is already in hand.

  • with(['team', 'lines']) is the graph this page reads.
  • A constraint inside with() applies to each invoice's lines.
  • limit() inside an eager load keeps that many lines per invoice, from Laravel 11 on.
  • loadMissing() fills what is absent. load() queries again.

Check yourself · 3 questions

Question 1 of 3The view reads $invoice->team and you did not eager load team. What happens with the lazy-load guard off?

The list loads the relations it will touch. Next, make a forgotten one throw before a customer sees it.

← PreviousLocal and global scopes

On this page

  • Load the graph you already know
  • loadMissing when you already hold the invoice
  • Recap
  • Check yourself

Eloquent, properly

  • 1 Overview

Shape the data

  • 2 Models that match the domain
  • 3 Migrations you can still read next year
  • 4 Casts, enums and custom casters

Connect it

  • 5 One to many, without the surprises
  • 6 Belongs-to and whereBelongsTo
  • 7 Many to many and pivot data
  • 8 Has many through

Query it safely and fast

  • 9 Local and global scopes
  • 10 Eager loading on purpose
  • 11 Catching N+1 queries
  • 12 Indexes, select and chunking
  • 13 API resources without leaking attributes

Eloquent, properly

Lessons

  1. 01Overview

Shape the data

  1. 02Models that match the domain
  2. 03Migrations you can still read next year
  3. 04Casts, enums and custom casters

Connect it

  1. 05One to many, without the surprises
  2. 06Belongs-to and whereBelongsTo
  3. 07Many to many and pivot data
  4. 08Has many through

Query it safely and fast

  1. 09Local and global scopes
  2. 10Eager loading on purpose
  3. 11Catching N+1 queries
  4. 12Indexes, select and chunking
  5. 13API resources without leaking attributes

Get the Friday email

One snippet and the week’s Laravel news, in your inbox every Friday morning. Unsubscribe whenever you like.

laravelmastery

Practical Laravel learning for working developers, made in the UK.

Learn

  • Snippets
  • Articles
  • Series

Stay current

  • News
  • Friday email
  • RSS feed

Company

  • About
  • Privacy
  • Terms
© 2026 Laravel Mastery. Independent and not affiliated with Laravel Holdings Inc.Designed and built by Half Shell Studios

We remember the videos you watch, where you left off, and which series lessons you mark as read, against this connection’s address. Nothing is stored until you agree.