Optimizing queries using Eloquent relationships

Imagine you're managing an e-commerce platform and want to identify customers who haven't made a purchase in your store. This might be useful for targeted marketing campaigns or identifying inactive accounts. There are many approaches you can take, all of which will fit nicely into two categories: downright horrible approaches; and then optimum approaches. We will be looking at an example of each.

We will discuss the following:

  • Chaining Eloquent relationships on existing query.
  • Using doesntHave Eloquent method for optimum query performance (specifically, preventing N+1 issues).
  • Bonus tip: how to write more maintainable code by offloading logical checks from Blade templates to controllers.

As mentioned above, we will be using a case of an e-commerce platform where you need to get Users that do not have Orders.

Before diving into the optimized approach, let's look at a naive method where we fetch all users and check for orders in the Blade template.

1. Naive Approach

// UserController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;

class UserController extends Controller
{
    public function inactiveUsers()
    {
        $users = User::paginate(10);

        return view('user.inactive-users', compact('users'));
    }
}
<!-- user/inactive-users.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
    <h1>Inactive Users</h1>
    @foreach($users as $user)
        @if ($user->orders()->count() === 0)
            <div>
                <p>Name: {{ $user->name }}</p>
            </div>
        @endif
    @endforeach

    {{ $users->links() }}
</div>
@endsection

The code above nicely leverages the existing orders relationship, chaining on it to generate a new query with $users->orders() in the blade file. However, this approach is not efficient.

Issues with this Approach:

  • Inefficient Logic: The logic to check for users without orders is pushed into the view, which can lead to performance issues, especially with large datasets as is typical of an e-commerce store.

  • View Cluttered: Mixing business logic in Blade templates makes them harder to maintain.

2. Optimized Approach

By leveraging Eloquent's doesntHave method directly in the query, we can efficiently filter users without orders directly in the controller, keeping our views clean and performance optimized.

// UserController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;

class UserController extends Controller
{
    public function inactiveUsers()
    {
        $users = User::doesntHave('orders')->paginate(10);

        return view('user.inactive-users', compact('users'));
    }
}
<!-- user/inactive-users.blade.php -->
@extends('layouts.app')

@section('content')
<div class="container">
    <h1>Inactive Users</h1>
    @foreach($users as $user)
        <div>
            <p>Name: {{ $user->name }}</p>
        </div>
    @endforeach

    {{ $users->links() }}
</div>
@endsection

We get these from the optimized approach shown in the code above:

  • Cleaner Views: By handling logic in the controller, views become cleaner and easier to maintain. Your teammates and even your future self will thank you!

  • Performance Improvement: Leveraging doesntHave ensures efficient querying, enhancing performance.

Conclusion

Using the doesntHave method, we can simplify our codebase, enhance maintainability, and improve overall application performance. These are essential for building robust and scalable applications. You should always look out for Eloquent eager loading practices that can help optimize query performance.

Wanna chat about what you just read, or anything at all? Click here to tweet at me on 𝕏