Laravel 11 - N+1 Problem - Lazy vs Eager Loading

Touseef Afridi
22 Oct 24

Laravel 11 - N+1 Problem - Lazy vs Eager Loading

In this tutorial, we will discuss the N+1 problem in Laravel 11 and explore its implications for query performance and application efficiency.


If you're a video person, feel free to skip the post and check out the video instead!


Quick Overview

This guide explores the N+1 problem in Laravel 11 through practical examples, starting with creating a Post model, migration, and factory. It then defines relationships between the User and Post models, runs migrations, and seeds the database with users and posts. The N+1 problem is demonstrated by lazy loading, where additional queries are made for each related record. The solution is presented with eager loading, which fetches users and their posts in a single query, optimizing performance. The guide concludes with advice on when to use lazy loading versus eager loading based on how often related data is accessed.
Note : We will explore the N+1 problem in Laravel 11 through practical examples. We'll walk through the process of creating a Post model, defining relationships, and observing how lazy loading can lead to excessive queries. Then, we'll see how eager loading can optimize our queries effectively. Let’s dive in!

Step # 1 : Create Post Model, Migration & Factory.

Laravel provides the User model, migration, and factory by default, so there's no need to create them manually. To create the Post model, migration file, and factory in one go, simply run the following command.
php artisan make:model Post -mf
This command will generate the Post model at app/Models/Post.php to interact with the posts table, a migration file in database/migrations to define the table structure, and a factory file at database/factories/PostFactory.php for generating fake data during testing or seeding. With these components in place, you're ready to create and manage posts in your Laravel application.

Step # 2 : Update Post Migration.

In the create_posts_table.php migration file, you'll define the necessary fields and set up a foreign key to reference the user. The up() method should look like this.
public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->onDelete('cascade'); // Foreign key for user
        $table->string('title');
        $table->text('content');
        $table->timestamps();
    });
}
This setup creates the posts table with an id, user_id (foreign key referencing the user), title, content, and timestamps. The foreign key ensures each post is linked to a user, and onDelete('cascade') will delete posts when the associated user is removed.

Step # 3 : Run the Migration.

Run the following command to apply the migration and create the posts table
php artisan migrate
This command will execute the migration, creating the posts table with the defined fields (id, user_id, title, content, and timestamps) in your database. The user_id will be set as a foreign key, establishing the relationship between posts and users.

Step # 4 : Define Relationships.

In this step, you'll define the relationships between the User and Post models. In the User model (User.php), add the following method to indicate that a user can have many posts.
public function posts() // A user can have many posts
{
    return $this->hasMany(Post::class);
}
In the Post model (Post.php), define the relationship to indicate that each post belongs to a user.
public function user() // Each post belongs to a user
{
    return $this->belongsTo(User::class);
}
This setup establishes a one-to-many relationship a user can have multiple posts, and each post is associated with a single user.

Step # 5 : Update PostFactory.php.

To generate fake records for posts, update the PostFactory.php file as follows.
<?php
namespace Database\Factories;
use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
    protected $model = Post::class;
    public function definition()
    {
        return [
            'title' => $this->faker->sentence,
            'content' => $this->faker->paragraph,
        ];
    }
}
This configuration allows the factory to generate random titles and content for posts using Faker when seeding the database.

Step # 6 : Update DatabaseSeeder.php.

To seed your database with users and their associated posts, modify the DatabaseSeeder.php file as follows.
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Post;
class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Create 10 users and for each user, create 3 posts
        User::factory(10)->create()->each(function ($user) {
            Post::factory(5)->create(['user_id' => $user->id]); // Associate posts with user
        });
    }
}
This will create 10 users, and for each user, it will generate 5 posts, associating each post with the respective user using the user_id.

Step # 7 : Run the Seeder.

To populate your database with the data defined in your seeder, execute the following command.
php artisan db:seed
This will run the DatabaseSeeder.php file, creating 10 users, each with 5 associated posts, as defined in the seeder.

Step # 8 : Let's See N+1 Problem in Action.

Start the Laravel development server by running the following command.
php artisan serve
Then, access the following URL.
127.0.0.1:8000
I’m using the Laravel Debugbar tool to monitor how many queries are being executed. This will help you observe the N+1 query issue in action when fetching related data without eager loading. The Laravel Debugbar will display detailed information about each query executed on the page, allowing you to pinpoint any N+1 problems.

Lazy Load
Let's update the route to use lazy loading and demonstrate the N+1 problem in action.
Route::get('/', function () {
    $users = User::all(); // 1 query to fetch all users
    foreach ($users as $user) {
        echo $user->posts; // N queries to fetch posts for each user on demand (N = number of users)
    }
    return view('welcome');
});
In this example, the User::all() query fetches all users in a single query. However, when we try to access each user's posts with $user->posts, an additional query is executed for each user, resulting in N queries (where N is the number of users). The Laravel Debugbar will show the total number of queries executed, helping you visualize the N+1 problem in action. Refer to the image below to see the breakdown of queries.

Eager Loading
By using eager loading, we can resolve the N+1 problem by fetching all users and their posts in a single query. Here's how you can implement it.
Route::get('/', function () {
    $users = User::with('posts')->get(); // **1 query to fetch all users and their posts**
    foreach ($users as $user) {
        echo $user->posts; // No additional queries are made for posts
    }
    return view('welcome');
});
In this case, the User::with('posts') method loads both the users and their related posts in one query, eliminating the need for N additional queries. The Laravel Debugbar will show the total number of queries executed. Refer to the image below to see the optimized query count.

Lazy loading should be used when related data is accessed infrequently, as it helps optimize resource usage by only fetching related data when it’s actually needed. On the other hand, eager loading is ideal when you need related data upfront, as it improves performance by reducing the number of queries and preventing the N+1 query problem, where multiple queries are made for each related record. Eager loading fetches all related data in a single query, making it more efficient in scenarios where the related data will be used immediately.

Conclusion

By following this guide, you've successfully explored the N+1 problem in Laravel 11 and learned how to optimize your application using eager loading. You've seen how lazy loading can lead to excessive queries and how eager loading can resolve this issue by fetching related data efficiently in a single query. This approach significantly improves the performance of your application, especially when dealing with large datasets. By understanding when to use lazy loading versus eager loading, you can make informed decisions to optimize your application's resource usage and performance.

For more details, refer to the Laravel documentation on Eloquent relationships and query optimization.

Share this with friends!


"Give this post some love and slap that 💖 button as if it owes you money! 💸😄"
0

0 Comments

To engage in commentary, kindly proceed by logging in or registering