Eloquent relationships and loading
Status: Complete. Last reviewed 2026-08-27.
Precise mental model
Section titled “Precise mental model”An Eloquent relationship is a model method that constructs a relation object: a specialized query builder containing key-matching rules between two model types. It does not mean related rows are already present. Calling $post->comments() returns that query-capable relation; reading $post->comments asks the model for the relationship result, lazy-loading it if necessary and then caching it in the model’s loaded-relations array.
This distinction turns loading into part of application correctness. The same property access may be a memory read on one code path and a database query on another. A loop, resource, accessor, or log context that appears computationally cheap can therefore produce hundreds of queries. Eager loading changes the query plan, not the relationship definition.
Relationships also do not create database integrity. A belongsTo() declaration does not add a foreign key, an index, uniqueness, or cascade behavior. Migrations own those constraints. Policies and tenant rules still own access. Model mechanics such as casts, dirty tracking, events, and mass assignment are covered in Eloquent model mechanics.
Relationship shape and key ownership
Section titled “Relationship shape and key ownership”Choose a relationship from the data invariant, not from the method name you remember:
belongsTois declared on the model holding the foreign key; its inverse is usuallyhasOneorhasMany.hasOneandhasManyquery a related table whose foreign key points back to the parent.- one-of-many relationships select one related row by an aggregate ordering, such as the latest priced offer, without loading the full collection.
- through relationships traverse a known intermediate model without exposing that model as a separately loaded step.
belongsToManyuses an intermediate table; Eloquent exposes selected intermediate columns through a pivot object.- polymorphic relationships store both an identifier and a type so one association can target several model tables.
Conventions infer keys, but production code should make non-conventional keys obvious and back them with indexes and constraints. Cardinality is a database fact: a claimed one-to-one needs a unique constraint on the foreign key if duplicates must be impossible. Nullable foreign keys, delete actions, and composite tenant-aware constraints are schema decisions, not Eloquent defaults.
A many-to-many intermediate row may be more than glue. Use withPivot() to retrieve needed columns and withTimestamps() when those timestamps matter. A custom pivot model can add casts and focused behavior, but Laravel pivot models cannot use SoftDeletes; an association with its own lifecycle, soft deletion, permissions, or external identity is often clearer as a normal Eloquent model. attach, detach, sync, and pivot updates have distinct destructive and event consequences, so treat synchronization from client arrays as a deliberate write operation.
Polymorphism trades schema simplicity for weaker database-level referential integrity and more complex query planning. By default the type column stores a fully qualified class name. Relation::enforceMorphMap() stores stable aliases and decouples rows from PHP namespaces, but adding a morph map to existing data requires migrating old type values. Eager-loading a morphTo issues queries per concrete type; morphWith, constrain, and loadMorph let each type receive its own nested plan.
Relation method, loaded property, and cache state
Section titled “Relation method, loaded property, and cache state”These expressions answer different questions:
$post->comments()->where('approved', true)->exists(); // database query$post->comments; // loaded collection$post->relationLoaded('comments'); // cache-state checkThe method path is for composing SQL, aggregates, existence checks, or writes. The property path is for consuming the already-loaded result—or accepting a lazy query. Once loaded, the property returns the cached relationship until it is explicitly reloaded or unset. Creating a comment through another object does not automatically refresh $post->comments; stale relationship collections are another form of stale model state.
Parentheses are therefore consequential. $post->comments()->count() asks the database for a count. $post->comments->count() loads all related models and counts them in PHP unless they were already loaded for another reason. Neither is universally better: if the response needs the comment models anyway, reuse the collection; if it needs only a count, avoid hydrating it.
Relationship queries retain their key constraint, but additional orWhere clauses must be grouped. An ungrouped orWhere can escape the parent constraint and return another parent’s rows. Inspect generated SQL and bindings when a relationship query returns more data than its name implies.
Choosing a loading strategy
Section titled “Choosing a loading strategy”with() declares relationships while building the parent query. Eloquent normally executes the parent query and then one additional query per eager-loaded relationship level, matching results back by keys. Nested and constrained eager loads make the intended graph and filters explicit at the use-case boundary.
load() performs lazy eager loading after a model or collection has already been retrieved. It is appropriate when a later decision determines the response shape. It reloads the named relationship even if it is present. loadMissing() loads only absent relationships, making it useful in composable enrichment code that should preserve a caller’s already-constrained relation. That preservation can also be surprising: a relation loaded earlier with a narrow constraint remains narrow.
Default $with loading suits a small relationship genuinely needed on almost every path, but raises the cost of commands, queue serialization preparation, and endpoints that never use it. without() or withOnly() can revise defaults per query, yet frequent opt-outs are evidence that the model default is too broad.
Laravel 13 also supports opt-in automatic eager loading globally or on one Eloquent collection. Accessing a missing relationship on one member can lazy-eager-load it across the collection. This can remove incidental N+1 queries in exploratory application code, but it makes query initiation less explicit and can load large graphs unexpectedly. Treat query counts, row counts, memory, and response shape as the decision evidence.
Filtering, aggregates, and avoiding over-fetching
Section titled “Filtering, aggregates, and avoiding over-fetching”Loading children and filtering parents are separate operations. with('posts') loads posts for selected users; it does not require that a user has posts. has and whereHas constrain parents by related-row existence. When the same child predicate should both select parents and define the loaded children, withWhereHas prevents the two constraints from drifting.
Use withCount, withExists, withSum, and related aggregate methods when the operation needs facts rather than child objects. Their result is added as an attribute without hydrating the relation collection. When combining a custom select with withCount, call withCount after select so the aggregate selection is not overwritten.
Eager loading removes one query pattern; it does not guarantee an efficient plan. Common over-fetching failures include:
- loading every comment when the response needs only a count or existence flag;
- loading an unbounded nested graph for a paginated parent list;
- applying a constraint to
whereHasbut loading all children with a differentwithconstraint; - selecting too few columns and omitting the primary/foreign keys Eloquent needs to match results;
- eager-loading a polymorphic target and then triggering per-type nested lazy loads;
- loading thousands of rows into memory merely to avoid additional round trips.
Pagination or bounded parent selection comes before graph loading. Select required columns, including matching keys, constrain child sets to the response contract, and prefer a dedicated query or projection when a report does not need Active Record objects. “Two queries” can still perform poorly when they return and hydrate a vast object graph; query count is a signal, not the objective.
N+1 diagnosis and reverse traversal
Section titled “N+1 diagnosis and reverse traversal”The classic N+1 starts with one parent query followed by one lazy relationship query for each parent. Capture SQL with Laravel’s database listeners, Telescope, Pulse, a profiler, or a test query counter; group repeated SQL shapes and preserve route, job, and dataset context. A local test with one record cannot reveal growth, so seed several parents with uneven child counts.
Then trace the first relationship access. It may be visible in a Blade loop, but it may also hide in an API resource, an appended accessor, a policy, a notification, a log formatter, or recursive JSON serialization. Fix the owning query boundary with a precise eager load rather than adding unrelated default loads to the model.
Eager-loading children does not necessarily hydrate their parent relation. In a nested loop over $post->comments, reading $comment->post can still query once per comment. Laravel 13’s chaperone() on supported hasMany and morphMany relations populates the known parent on each child, either in the relationship definition or for a specific eager load. Alternatively, restructure the loop or load the inverse explicitly. Choose based on whether reverse traversal is a stable requirement.
Model::preventLazyLoading() turns accidental lazy access into a violation, commonly outside production; a custom handler can log rather than throw. Strictness catches an unplanned query but does not design the correct eager-load graph. Production-only code paths and datasets still need telemetry and query-budget tests.
Serialization is a query boundary
Section titled “Serialization is a query boundary”Eloquent toArray() and JSON serialization recursively include attributes and relationships that are already loaded. Serialization does not need to load every defined relationship, but code preparing the representation can. Three common query triggers are direct relationship property access in a resource, an appended accessor that reads a relationship, and nested resource/collection code that assumes each child has another relation.
API resources should make the contract conditional on prepared data. whenLoaded('author'), whenCounted('comments'), whenAggregated(...), and whenPivotLoaded(...) omit fields whose inputs were not loaded; they do not fetch those inputs. The controller or application query owns includes, constraints, and authorization, while the resource owns representation. Passing $this->author into whenLoaded would access the property too early; pass the relationship name as documented.
Also separate hiding from loading. $hidden controls output, not query execution or memory. Loading sensitive or huge relations and then hiding them still pays the cost and may expose them to internal code. For public APIs, explicit resources are safer than returning recursively serialized models whose shape changes when upstream code happens to load another relationship.
Failure timeline: a “fixed” endpoint still scales poorly
Section titled “Failure timeline: a “fixed” endpoint still scales poorly”- An index endpoint loads 100 projects and lazily reads
tasks, producing 101 queries. - A developer adds
with('tasks.assignee'), reducing query count to three. - Each project has thousands of historical tasks, so hydration time and memory spike; most tasks are not part of the response.
- A resource accesses
task->commentsand silently introduces another N+1 in serialization. - Query-count monitoring alone reports an improvement while latency and memory regress.
- The repair paginates projects, loads only bounded active tasks and required columns, uses counts for totals, conditionally includes prepared relationships, and tests query count plus response cardinality and memory-relevant row volume.
Current and legacy context
Section titled “Current and legacy context”Current (Laravel 13): automatic eager loading may be enabled globally or per collection; chaperone() supports parent hydration for one-to-many and polymorphic one-to-many traversal; API resources expose conditional relationship, count, aggregate, and pivot helpers; and PHP attributes are available for several model declarations.
Common (Laravel 11–12): explicit with, load, loadMissing, constrained/nested loads, aggregate loading, morph maps, lazy-loading prevention, and resource whenLoaded remain the normal vocabulary. Codebases differ in whether newer automatic loading and parent hydration are adopted.
Legacy: older applications may return models directly, store fully qualified classes in polymorphic type columns, or rely on pervasive lazy loading. Introduce resources, morph maps, and strict-loading diagnostics behind characterization tests; changing relation serialization or morph values can break API payloads and existing rows even when PHP code still compiles.
Interview practice
Section titled “Interview practice”- LARAVEL-RELATIONSHIPS-01 — Contrast a relationship method and property
- LARAVEL-RELATIONSHIPS-02 — Choose a relationship shape
- LARAVEL-RELATIONSHIPS-03 — Choose
with,load, orloadMissing - LARAVEL-RELATIONSHIPS-04 — Diagnose a hidden N+1
- LARAVEL-RELATIONSHIPS-05 — Fix eager-loading over-fetching
- LARAVEL-RELATIONSHIPS-06 — Design a relationship-safe API resource