Tips to Improve Laravel Eloquent Performance with Eager loading

Eloquent performance patterns are tactics and approaches that may be used to improve the performance of an application that employs the Object-Relational Mapping (ORM) component of Laravel Eloquent to communicate with a database.

Here are some pointers for enhancing a Laravel application’s speed with Eloquent:

  • Reduce the number of database requests by using eager loading: Instead of creating separate queries for each model, eager loading enables you to load related models in a single query.
  • Optimize cache invalidation by using cache tags: Cache tags make it simpler to maintain a fresh and accurate cache by allowing you to define which cached items should be invalidated when a certain model is changed.
  • Process huge datasets using chunking for improved efficiency and less strain on your server’s RAM. Chunking enables you to process large datasets in smaller increments.
  • Utilize indexes to enhance database queries: By enabling the database to more rapidly discover the data it requires, indexes can assist to enhance query performance.
  • Monitor and improve database queries: To monitor and improve database queries for improved efficiency, use tools like Laravel’s built-in query logger or the Laravel Debugbar package.
  • These techniques will help your Laravel application run better, be more productive, and be more scalable.

Let’s write some code and test it with the laravel-debugbar

Create a Laravel Project

laravel new eloquent_performance

Install debug bar

composer require barryvdh/laravel-debugbar –dev

Let’s establish a table for users and organizations where users are members of organizations.

PHP artisan make:model Organization -mcr

The organizations and users table will now be modified.

Schema::create('organizations', function (Blueprint $table) {
         $table->id();
         $table->string('name');
         $table->timestamps();
     });
Schema::create('users', function (Blueprint $table) {
         $table->id();
         $table->foreignId('organization_id');
         $table->string('name');
         $table->string('email')->unique();
         $table->timestamp('email_verified_at')->nullable();
         $table->string('password');
         $table->rememberToken();
         $table->timestamps();
     });
run -> php artisan migrate

We will now use Factory and Seeder to generate some fictitious data.

Organization Factory

<?php

namespace Database\Factories;

use App\Models\Organization;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model>
 */
class OrganizationFactory extends Factory
{
   protected $model = Organization::class;
   /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
         'name'    => $this->faker->name(),
        ];
    }
}

Organization Seeder

class OrganiationSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
      Organization::factory(1000)->create()->each(fn ($org) => $org->users()
         ->createMany(User::factory( 50)->make()->map->getAttributes())
      );
    }
}

We will now update DatabaseSeeder to include Organization Seeder.

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
       $this->call(OrganiationSeeder::class);

    }
}

We shall now execute PHP artisan db:seed.

We are adding 50,000 users from 1,000 different organizations.

Let’s build a new user controller.

PHP artisan make:controller UserController -r

Now will add a route named users also a user list method on controller

Route::resource('/users',UserController::class);

We will now load the user list into the controller.

  public function index()
  {
   //DB::enableQueryLog(); // Enable query log
 $users=User::simplePaginate();
//dd(DB::getQueryLog()); // Show results of log
return  view('users')->with(['users'=>$users]);
  }

If we look at the debug bar, we can see that it takes 27.64 milliseconds to load 1 view, 16 queries, and 31 models. Thus, it generates a N+1 difficulty.

How then can we optimize it? Let’s give eager loading a go.

What does Laravel’s eager loading mean?

As opposed to loading related models individually in a separate query, Laravel’s eager loading approach enables you to load related models as part of the primary query. By lowering the number of queries that must be run, can assist to enhance the performance of your application.

With Laravel, you can utilize the with method on a query builder instance to implement eager loading. you can eagerly load the organization as follows:

class UserController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
       //DB::enableQueryLog(); // Enable query log
       $users=User::with('organization')->simplePaginate();
      //dd(DB::getQueryLog()); // Show results of log
      return  view('users')->with(['users'=>$users]);
    }}

We are receiving the user list and associated organization from the code above.

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
<body style="margin: 20px">
@isset($users)

<table class="table">
    <thead>
    <tr>
        <th scope="col">#</th>
        <th scope="col">Name</th>
        <th scope="col">Email</th>
        <th scope="col">Organization</th>
    </tr>
    </thead>
    <tbody>
    @foreach($users as $user)

        <tr>
        <th scope="row">{{$user->id}}</th>
        <td>{{$user->name}}</td>
        <td>{{$user->email}}</td>
        <td>{{$user->organization->name}}</td>
    </tr>
    @endforeach

    </tbody>
</table>

    @endisset

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
</body>
</html>

Currently, the debug bar shows 1 view, 2 queries, 17 models loaded, 20 MB of RAM consumed, and a time of 23.55 milliseconds.

Let’s imagine that the name’s data has to be displayed as ascending.

Therefore, we must include orderby in the query.

Therefore, we can observe that it is taking longer and that 32 models are loading. As a result, the name column could require an index.

PHP artisan make:migration add_name_index_to_users

let’s add the index to the newly created migration

Schema::table('users', function (Blueprint $table) {
    $table->index('name');
});

Now run php artisan migrate

Because of the index, running the view file again will be significantly quicker than the first time.

In order to create eager load relationships, we can do so by default; in such case, the relationship method name must be added to the model.

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Organization extends Model
{
    use HasFactory;
   protected $fillable = [
      'name'
   ];
   protected $with=['user'];

   public function user()
   {
      return $this->hasMany(User::class);
   }
}
Now run http://127.0.0.1:8000/users

However, 832 models can be seen loading, and the loading time has grown. So, let’s stay away from that.

Laravel Eager loading with condition

$users=User::with(['organization' => function($query) {
$query->where('organizations.id', 2013);
}])
   ->orderBy('name')
     ->simplePaginate();
return  view('users')->with(['users'=>$users]);

Change the code in Laravel Blade as certain users won’t have oragnaizion->name.

@isset($user->organization->name)
    {{$user->organization->name}}
@endisset

Now we can see that the company is only loading once.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top