Laravel Eloquent Eager Loading
19 May 2025 | Category: Laravel
Eager loading in Laravel’s Eloquent ORM is a technique to load related data alongside the main model in a single query, preventing the N+1 query problem. This improves performance by reducing the number of database queries, especially when accessing relationships like one-to-one, one-to-many, or many-to-many. This SEO-friendly, plagiarism-free guide explains eager loading, how it works, and how to implement it effectively, with practical examples and best practices. Based on Laravel 11 (as of May 19, 2025), this tutorial builds on previous discussions about Eloquent models and relationships and is designed for beginners and intermediate developers.
What is Eager Loading?
Eloquent relationships (e.g., hasMany
, belongsTo
, belongsToMany
) allow models to access related data. By default, relationships are lazy loaded, meaning related data is queried only when accessed, which can lead to multiple queries (the N+1 problem). Eager loading loads all related data upfront in a single query (or a few queries), optimizing performance.
The N+1 Query Problem
- Scenario: Fetching 10 posts and their associated users.
- Lazy Loading:
- 1 query to fetch 10 posts.
- 10 additional queries (one per post) to fetch each user.
- Total: 11 queries.
- Eager Loading:
- 1 query to fetch 10 posts.
- 1 query to fetch all related users.
- Total: 2 queries.
Key Methods for Eager Loading
with()
: Eagerly load relationships when querying.load()
: Eagerly load relationships on an already-retrieved model or collection.withCount()
: Load the count of related records without fetching the records themselves.
How Eager Loading Works
Eloquent uses the with()
method to specify which relationships to load alongside the main query. For example, when fetching Post
models, you can eager load their User
and Tags
relationships to avoid separate queries.
Basic Syntax
$posts = Post::with('user')->get();
- Breakdown:
Post::get()
: Fetches all posts.with('user')
: Includes the relatedUser
for each post in one query.
Example Database Schema
For clarity, we’ll use the following tables (from previous discussions):
users
:id
,name
,email
.posts
:id
,title
,content
,user_id
,created_at
,updated_at
.tags
:id
,name
.post_tag
:post_id
,tag_id
(pivot table).comments
:id
,post_id
,content
.
Models:
User
: Has manyPosts
.Post
: Belongs toUser
, has manyComments
, belongs to manyTags
.Tag
: Belongs to manyPosts
.Comment
: Belongs toPost
.
Implementing Eager Loading
1. Eager Loading with with()
Load related data when querying the main model.
Example: Fetch posts with their users.
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->title . ' by ' . $post->user->name; // No additional queries
}
- Queries:
- 1:
SELECT * FROM posts
. - 1:
SELECT * FROM users WHERE id IN (user_ids_from_posts)
.
Multiple Relationships:
$posts = Post::with(['user', 'tags'])->get();
foreach ($posts as $post) {
echo $post->title . ' by ' . $post->user->name;
echo 'Tags: ' . $post->tags->pluck('name')->join(', ');
}
- Queries:
- Posts query.
- Users query.
- Tags query (via pivot table).
2. Nested Eager Loading
Load relationships of related models using dot notation.
Example: Load posts, their users, and each user’s profile.
$posts = Post::with('user.profile')->get();
foreach ($posts as $post) {
echo $post->title . ' by ' . $post->user->name;
echo 'Bio: ' . $post->user->profile->bio;
}
- Dot Notation:
user.profile
loads theprofile
relationship of theuser
relationship. - Queries:
- Posts query.
- Users query.
- Profiles query.
3. Eager Loading Specific Columns
Optimize queries by selecting only needed columns.
Example:
$posts = Post::with(['user' => fn ($query) => $query->select('id', 'name')])
->select('id', 'title', 'user_id')
->get();
- Note: Include foreign key (
user_id
) in the main query to link relationships. - Queries:
SELECT id, title, user_id FROM posts
.SELECT id, name FROM users WHERE id IN (...)
.
4. Eager Loading with Conditions
Apply constraints to related models.
Example: Load posts with only published comments.
$posts = Post::with(['comments' => fn ($query) => $query->where('is_approved', true)])
->get();
foreach ($posts as $post) {
echo $post->title . ' has ' . $post->comments->count() . ' approved comments';
}
5. Eager Loading Counts
Use withCount()
to load the count of related records.
Example: Count comments for each post.
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->title . ' has ' . $post->comments_count . ' comments';
}
- Result: Adds a
comments_count
attribute to eachPost
. - Queries:
- Posts query.
- Count query for comments.
Conditional Count:
$posts = Post::withCount([
'comments',
'comments as approved_comments_count' => fn ($query) => $query->where('is_approved', true)
])->get();
echo $post->comments_count; // Total comments
echo $post->approved_comments_count; // Approved comments
6. Lazy Eager Loading with load()
Load relationships on already-retrieved models or collections.
Example:
$posts = Post::get(); // Fetch posts without relationships
$posts->load('user', 'tags'); // Load relationships later
foreach ($posts as $post) {
echo $post->title . ' by ' . $post->user->name;
}
- Use Case: When relationships are needed conditionally after the initial query.
7. Eager Loading in Relationships
Eager load when querying related models.
Example: Load user with their posts and comments.
$user = User::with(['posts.comments'])->find(1);
foreach ($user->posts as $post) {
echo $post->title . ' has ' . $post->comments->count() . ' comments';
}
Preventing Lazy Loading
To enforce eager loading and catch N+1 issues, use strict mode or disable lazy loading.
Strict Mode (Laravel 10+)
Prevent lazy loading in development.
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Model::preventLazyLoading(!app()->isProduction());
}
}
- Throws an exception if relationships are lazy loaded (e.g.,
$post->user
withoutwith('user')
).
Disable Lazy Loading Entirely
Model::preventLazyLoading();
Best Practices for Eager Loading
- Always Eager Load Relationships:
- Use
with()
for any relationships you plan to access to avoid N+1 issues. - Example:
Post::with('user')->get()
.
- Select Specific Columns:
- Reduce data transfer by selecting only needed columns:
php Post::with(['user' => fn ($query) => $query->select('id', 'name')])->get();
- Use withCount for Counts:
- Prefer
withCount()
over loading entire relationships for counts.
- Leverage Nested Loading:
- Use dot notation for deep relationships (e.g.,
user.profile
).
- Apply Conditions Sparingly:
- Avoid overly complex constraints in
with()
to keep queries readable. - Move complex logic to query scopes:
php public function scopeWithApprovedComments($query) { return $query->with(['comments' => fn ($q) => $q->where('is_approved', true)]); }
- Test Performance:
- Use Laravel Debugbar or query logging to monitor query count:
php DB::enableQueryLog(); Post::with('user')->get(); dd(DB::getQueryLog());
- Enable Strict Mode:
- Use
Model::preventLazyLoading()
in development to catch N+1 issues.
- Document Relationships:
- Clarify eager-loaded relationships in PHPDoc: “`php /**
- Get posts with their users and tags.
*/
public function getPostsWithRelations()
{
return Post::with([‘user’, ‘tags’])->get();
}
“`
- Get posts with their users and tags.
Debugging Eager Loading
- N+1 Issues:
- Check query logs to confirm the number of queries:
php DB::enableQueryLog(); dd(DB::getQueryLog());
- Ensure
with()
includes all accessed relationships. - Missing Relationships:
- Verify relationship methods (e.g.,
public function user()
). - Check foreign keys (e.g.,
user_id
inposts
). - Performance Bottlenecks:
- Use Laravel Debugbar to identify slow queries.
- Optimize with specific columns or indexes.
- Incorrect Data:
- Ensure constraints in
with()
(e.g.,where()
) are correct. - Verify pivot table data for many-to-many relationships.
- Logs:
- Check
storage/logs/laravel.log
for errors.
Example: Complete Eager Loading Workflow
Step 1: Migrations
Users:
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
Posts:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->boolean('is_published')->default(false);
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
Tags:
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Post-Tag Pivot:
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
Comments:
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->text('content');
$table->boolean('is_approved')->default(false);
$table->timestamps();
});
Step 2: Models
User (app/Models/User.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
Post (app/Models/Post.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'content', 'is_published', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function scopeWithApprovedComments($query)
{
return $query->with(['comments' => fn ($q) => $q->where('is_approved', true)]);
}
}
Tag (app/Models/Tag.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
public function posts()
{
return $this->belongsToMany(Post::class);
}
}
Comment (app/Models/Comment.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
protected $fillable = ['content', 'post_id', 'is_approved'];
public function post()
{
return $this->belongsTo(Post::class);
}
}
Step 3: Controller
php artisan make:controller PostController
Controller (app/Http/Controllers/PostController.php
):
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
// Eager load user, tags, and approved comments
$posts = Post::with(['user', 'tags'])
->withCount('comments')
->withApprovedComments()
->where('is_published', true)
->get();
return view('posts.index', compact('posts'));
}
public function show(Post $post)
{
// Lazy eager load relationships
$post->load(['user', 'tags', 'comments' => fn ($query) => $query->where('is_approved', true)]);
return view('posts.show', compact('post'));
}
}
Step 4: Blade View
Index View (resources/views/posts/index.blade.php
):
@extends('layouts.app')
@section('content')
<h1>Posts</h1>
@forelse ($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
<p>By {{ $post->user->name }}</p>
<p>Tags: {{ $post->tags->pluck('name')->join(', ') }}</p>
<p>Comments: {{ $post->comments_count }}</p>
<p>Approved Comments: {{ $post->comments->count() }}</p>
</article>
@empty
<p>No posts found.</p>
@endforelse
@endsection
Show View (resources/views/posts/show.blade.php
):
@extends('layouts.app')
@section('content')
<h1>{{ $post->title }}</h1>
<p>By {{ $post->user->name }}</p>
<p>Tags: {{ $post->tags->pluck('name')->join(', ') }}</p>
<h3>Approved Comments</h3>
@forelse ($post->comments as $comment)
<p>{{ $comment->content }}</p>
@empty
<p>No approved comments.</p>
@endforelse
@endsection
Step 5: Routes
Routes (routes/web.php
):
use App\Http\Controllers\PostController;
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
Step 6: Run and Test
- Run migrations:
php artisan migrate
- Seed data (e.g., via
tinker
):
$user = App\Models\User::create(['name' => 'John', 'email' => 'john@example.com']);
$post = App\Models\Post::create([
'title' => 'Test Post',
'content' => 'Content',
'is_published' => true,
'user_id' => $user->id,
]);
$tag = App\Models\Tag::create(['name' => 'Laravel']);
$post->tags()->attach($tag->id);
$post->comments()->create(['content' => 'Great post!', 'is_approved' => true]);
- Access
/posts
or/posts/1
to view eager-loaded data.
Conclusion
Eager loading in Laravel’s Eloquent ORM is essential for optimizing database performance by reducing query counts. By using with()
, withCount()
, and load()
, you can efficiently load related data and avoid N+1 issues. Combining eager loading with query scopes, specific column selection, and strict mode ensures scalable and maintainable applications.
Next Steps:
- Add
with()
to queries:Post::with('user')->get()
. - Test
withCount()
for relationship counts. - Enable
preventLazyLoading()
in development.
For deeper insights, explore Laravel’s official documentation or connect with the Laravel community on platforms like X. Start optimizing your queries with eager loading today!
Laravel Eager Loading Example
This artifact provides a practical example of eager loading with a Post
model, including relationships, migrations, controller, and views.
Migrations
Users (database/migrations/2025_05_19_000001_create_users_table.php
)
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
Posts (database/migrations/2025_05_19_000002_create_posts_table.php
)
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->boolean('is_published')->default(false);
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
Tags (database/migrations/2025_05_19_000003_create_tags_table.php
)
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Post-Tag Pivot (database/migrations/2025_05_19_000004_create_post_tag_table.php
)
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
Comments (database/migrations/2025_05_19_000005_create_comments_table.php
)
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->text('content');
$table->boolean('is_approved')->default(false);
$table->timestamps();
});
Models
User (app/Models/User.php
)
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
Post (app/Models/Post.php
)
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'content', 'is_published', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function scopeWithApprovedComments($query)
{
return $query->with(['comments' => fn ($q) => $q->where('is_approved', true)]);
}
}
Tag (app/Models/Tag.php
)
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
public function posts()
{
return $this->belongsToMany(Post::class);
}
}
Comment (app/Models/Comment.php
)
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
protected $fillable = ['content', 'post_id', 'is_approved'];
public function post()
{
return $this->belongsTo(Post::class);
}
}
Controller (app/Http/Controllers/PostController.php
)
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
$posts = Post::with(['user', 'tags'])
->withCount('comments')
->withApprovedComments()
->where('is_published', true)
->get();
return view('posts.index', compact('posts'));
}
public function show(Post $post)
{
$post->load(['user', 'tags', 'comments' => fn ($query) => $query->where('is_approved', true)]);
return view('posts.show', compact('post'));
}
}
Views
Index (resources/views/posts/index.blade.php
)
@extends('layouts.app')
@section('content')
<h1>Posts</h1>
@forelse ($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
<p>By {{ $post->user->name }}</p>
<p>Tags: {{ $post->tags->pluck('name')->join(', ') }}</p>
<p>Comments: {{ $post->comments_count }}</p>
<p>Approved Comments: {{ $post->comments->count() }}</p>
</article>
@empty
<p>No posts found.</p>
@endforelse
@endsection
Show (resources/views/posts/show.blade.php
)
@extends('layouts.app')
@section('content')
<h1>{{ $post->title }}</h1>
<p>By {{ $post->user->name }}</p>
<p>Tags: {{ $post->tags->pluck('name')->join(', ') }}</p>
<h3>Approved Comments</h3>
@forelse ($post->comments as $comment)
<p>{{ $comment->content }}</p>
@empty
<p>No approved comments.</p>
@endforelse
@endsection
Usage
- Save migration, model, controller, and view files.
- Run migrations:
php artisan migrate
- Seed data in
php artisan tinker
:
$user = App\Models\User::create(['name' => 'John', 'email' => 'john@example.com']);
$post = App\Models\Post::create(['title' => 'Test Post', 'content' => 'Content', 'is_published' => true, 'user_id' => $user->id]);
$tag = App\Models\Tag::create(['name' => 'Laravel']);
$post->tags()->attach($tag->id);
$post->comments()->create(['content' => 'Great post!', 'is_approved' => true]);
- Access
/posts
or/posts/1
to view eager-loaded data.
This example demonstrates eager loading with with()
, withCount()
, and load()
, including nested relationships and query constraints.