skip to content

In Laravel 13, what does Model::automaticallyEagerLoadRelationships() change about lazy loading, and when would you still write with() yourself?

level: seniorimportance: should knowfreq 32%

answer

  1. opt-in, off by default
  2. first access loads for the whole collection
  3. nested access batches level by level
  4. withRelationshipAutoloading() per collection
  5. no constraints, no counts, no method calls

basics

~20 s

Model::automaticallyEagerLoadRelationships() turns a lazy load on any model from a collection into one batched load for every model in that collection. It fixes plain N+1 loops, but not constrained loads, counts, relation method calls or one-by-one fetches.

solid answer

~40 s

Called in `AppServiceProvider::boot()`, `Model::automaticallyEagerLoadRelationships()` gives every Eloquent collection an autoloader. The first time code reads an unloaded relation on one model, Eloquent loads that relation for **all** models of that class in the collection that lack it, one query, and the loaded children inherit the autoloader, so `$user->posts` then `$post->comments` in nested loops costs one query per level. `$collection->withRelationshipAutoloading()` enables the same for one collection. It is off by default in Laravel 13. You still write `with()` when you need a constraint, a column list, an aggregate like `withCount`, a load outside the view, or a query plan a reviewer can read; autoloading also does nothing for `$post->comments()->get()` or for models fetched one at a time.

go deeper

for a junior

Recall that the switch is opt-in, lives in AppServiceProvider::boot, and turns the first lazy load in a collection into one batched query for all models in it.

for a middle

Explain the per-collection batching, how nested relations inherit the autoloader, and the per-collection opt-in with withRelationshipAutoloading().

for a senior

Show the limits in production code: constraints, aggregates, relation method calls and one-by-one fetches, plus how the autoloader silences strict mode.

for a principal

Choose a stance for the codebase: explicit query plans with strictness, tolerant autoloading, or a staged mix, and how reviews and budgets enforce it.

## What the switch does Eloquent normally treats every model on its own: reading `$user->posts` on an unloaded relation runs a query for that one user. **Automatic eager loading** changes the unit of work from the model to the **collection it came from**: ```php use Illuminate\Database\Eloquent\Model; public function boot(): void { Model::automaticallyEagerLoadRelationships(); } ``` With the switch on, every Eloquent collection a query returns is given an autoload callback. When code reads an unloaded relation on any model in it, the callback loads that relation for every model **of the same class in the same collection** that does not have it yet, using a single where-in query, much like `$collection->loadMissing('posts')`. ## Nested access is batched too The loaded children inherit the autoloader. In the docs' own example: ```php $users = User::all(); foreach ($users as $user) { foreach ($user->posts as $post) { foreach ($post->comments as $comment) { echo $comment->content; } } } ``` Without the switch this runs one `posts` query per user and one `comments` query per post. With it, the first `$user->posts` loads posts for all users, and the first `$post->comments` loads comments for all posts reached through those users: three queries in total. To opt in for one collection instead of globally, call `withRelationshipAutoloading()` on it: `User::where('vip', true)->get()->withRelationshipAutoloading()`. A single model has the same method. ## Where it does not help The autoloader only answers one question: "this relation is unloaded, load it for my siblings". Everything else still needs an explicit decision: - **Constraints and columns.** It loads the whole relation. A filter (`with(['posts' => fn ...])`), a column list (`with('author:id,name')`) or a per-parent limit must be written by hand. - **Aggregates.** `$post->comments->count()` would autoload every comment just to count them; `withCount('comments')` or `loadCount('comments')` fetches only the number. - **Relation method calls.** `$post->comments()->latest()->first()` is an explicit query per post; the autoloader never sees it. - **One-by-one fetches.** Models retrieved in a loop with `find()` each live in their own collection of one, so there is nothing to batch across. - **Visibility.** The queries still happen inside the view, at access time. The page is faster, but a reviewer cannot read its data needs from the controller. ## What it costs - **Over-fetching.** Reading a relation on one model loads it for every model in the collection. A conditional read on the first of 500 rows still loads the relation for all 500. - **Whole rows.** The autoloader selects every column of the related table; wide tables cost memory that a column list would have saved. - **Deferred timing.** Queries run wherever the relation is first read, often in a Blade partial, so the query log no longer lines up with the controller. - **Global reach.** A static switch affects queue workers, console commands and exports too, which may prefer the old per-model behaviour or explicit loading. None of these is a reason to avoid it; they are the reasons explicit `with()` stays the default in code that is reviewed for performance. ## How it interacts with strict mode When a relation is read, Eloquent tries the autoloader **before** the lazy-loading check. If `preventLazyLoading()` is also on, relations the autoloader can serve never raise `LazyLoadingViolationException`. That makes the two switches partly exclusive in effect: strictness is a tool for finding missing eager loads, autoloading is a tool for tolerating them. Teams usually pick one stance per codebase: | Stance | Switches | Good fit | |---|---|---| | Explicit | `preventLazyLoading(! isProduction())`, `with()` everywhere | APIs and pages where query plans are reviewed | | Tolerant | `automaticallyEagerLoadRelationships()` | admin panels, internal tools, legacy views with deep loops | | Mixed | autoloading on selected collections via `withRelationshipAutoloading()` | a legacy page being migrated while the rest stays strict | ## How to answer in an interview State that the feature is opt-in and batched per collection, give the three-query nested example, and then list its limits: no constraints, no aggregates, no help for relation method calls or single fetches. The senior signal is the trade-off: it removes the most common N+1 without code changes, at the price of hiding the query plan and silencing the strict-mode alarm.

  • If automatic eager loading and preventLazyLoading() are both on, which one wins?
    The autoloader. When a relation is read, Eloquent first tries the autoload callback and only reaches the lazy-loading check if the relation is still unloaded afterwards. So for models from a collection, the relation is batched and no `LazyLoadingViolationException` is raised. Strict mode then only catches cases the autoloader cannot serve.
  • Why does autoloading not help a loop that calls Post::find($id) for each id?
    The autoloader batches across the collection a model came from. Each `find()` returns one model from its own one-row result, so a relation read on it can only be loaded for that one model. Fetch the posts together with `Post::whereKey($ids)->get()` and the relations can be batched, automatically or with `with()`.

saying these in an interview costs you the question

  • Laravel 13 enables automatic eager loading by default
  • It adds a join to the original query when a relation is first read
  • It applies constraints you would have written in a with() closure
  • It also batches $post->comments()->get() calls inside a loop
  • With it on, preventLazyLoading() throws even more often