Laravel

Laravel is a powerful and popular PHP framework that simplifies and accelerates web application development. Its elegant syntax, robust features like Eloquent ORM, Blade templating, and built-in security tools help developers create efficient and scalable apps. With strong community support and extensive documentation, Laravel is an ideal choice for both novice and experienced developers

Laravel Eloquent Eager Loading

19 May 2025 | Category:

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 related User 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 many Posts.
  • Post: Belongs to User, has many Comments, belongs to many Tags.
  • Tag: Belongs to many Posts.
  • Comment: Belongs to Post.

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 the profile relationship of the user 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 each Post.
  • 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 without with('user')).

Disable Lazy Loading Entirely

Model::preventLazyLoading();

Best Practices for Eager Loading

  1. Always Eager Load Relationships:
  • Use with() for any relationships you plan to access to avoid N+1 issues.
  • Example: Post::with('user')->get().
  1. Select Specific Columns:
  • Reduce data transfer by selecting only needed columns:
    php Post::with(['user' => fn ($query) => $query->select('id', 'name')])->get();
  1. Use withCount for Counts:
  • Prefer withCount() over loading entire relationships for counts.
  1. Leverage Nested Loading:
  • Use dot notation for deep relationships (e.g., user.profile).
  1. 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)]); }
  1. Test Performance:
  • Use Laravel Debugbar or query logging to monitor query count:
    php DB::enableQueryLog(); Post::with('user')->get(); dd(DB::getQueryLog());
  1. Enable Strict Mode:
  • Use Model::preventLazyLoading() in development to catch N+1 issues.
  1. 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();
      }
      “`

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 in posts).
  • 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

  1. Run migrations:
   php artisan migrate
  1. 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]);
  1. 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

  1. Save migration, model, controller, and view files.
  2. Run migrations:
   php artisan migrate
  1. 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]);
  1. 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.