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.
Eloquent, properly · Lesson 3
Eager loading on purpose
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.
$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.
$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.
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?