Polymorphic relationships — morphTo, morphMany, morphToMany
Concept
Eager loading solves the N+1 query problem — the most common performance issue in Eloquent applications. N+1 occurs when you load N records and then access a lazy-loaded relationship on each, causing N additional queries.
Lazy loading (default, dangerous):
$posts = Post::all(); // 1 query
foreach ($posts as $post) {
echo $post->author->name; // N queries (one per post)
}
// Total: 1 + N queriesEager loading with with():
$posts = Post::with('author')->get(); // 2 queries total
foreach ($posts as $post) {
echo $post->author->name; // no query — already loaded
}How eager loading works: Eloquent first queries all posts, collects all user_id values, then runs a single SELECT * FROM users WHERE id IN (1, 2, 3, ...). It matches results to models in PHP, not SQL JOINs.
Multiple eager loads: Post::with('author', 'tags', 'category')->get().
Nested eager loads: Post::with('author.profile', 'author.roles')->get().
Constrained eager loads: Post::with(['comments' => fn($q) => $q->where('approved', true)->latest()->limit(3)])->get().
load(): Lazy eager load on already-retrieved models: $post->load('comments').
loadMissing(): Load relationship only if not already loaded.
Preventing lazy loading: Model::preventLazyLoading(!app()->isProduction()) — throws exception in dev, logs in production.
Code Example
<?php
// N+1 problem — BAD
$posts = Post::all(); // 1 query: SELECT * FROM posts
foreach ($posts as $post) {
echo $post->author->name; // N queries: SELECT * FROM users WHERE id = ?
}
// Eager loading — GOOD
$posts = Post::with('author')->get(); // 2 queries total
$posts = Post::with('author', 'tags')->get(); // 3 queries
$posts = Post::with(['author', 'tags', 'category'])->get();
// Nested relationships
$users = User::with('posts.comments')->get(); // 3 queries
$users = User::with('posts.comments.author')->get(); // 4 queries
// Constrained eager load — filter the relationship
$posts = Post::with([
'comments' => function($query) {
$query->where('approved', true)
->orderBy('created_at', 'desc')
->limit(5);
},
'tags',
])->get();
// Eager load with specific columns (reduce data transfer)
$posts = Post::with(['author:id,name,avatar'])->get();
// Selects only id, name, avatar from users
// Load on an existing model
$post = Post::find(1);
$post->load('comments', 'tags'); // loads now
$post->loadMissing('comments'); // loads only if not already loaded
// Count without loading — withCount
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count; // available without loading comments
}
// withSum, withAvg, withMin, withMax
$users = User::withSum('orders', 'total')->get();
// $user->orders_sum_total — total value of all orders
// Prevent N+1 in development (app bootstrap)
\Illuminate\Database\Eloquent\Model::preventLazyLoading(
!app()->isProduction()
);
// Throws LazyLoadingViolationException when lazy loading is triggered