In Laravel 13, what does Model::automaticallyEagerLoadRelationships() change about lazy loading, and when would you still write with() yourself?
answer
- opt-in, off by default
- first access loads for the whole collection
- nested access batches level by level
- withRelationshipAutoloading() per collection
- no constraints, no counts, no method calls
basics
~20 sModel::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 sCalled 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
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.
Explain the per-collection batching, how nested relations inherit the autoloader, and the per-collection opt-in with withRelationshipAutoloading().
Show the limits in production code: constraints, aggregates, relation method calls and one-by-one fetches, plus how the autoloader silences strict mode.
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