Eager Load Relationships on an Existing Model in Laravel

Ep#30@Laracasts: Eager Load Relationships on an Existing Model

This post is a part of the Week X of 100DaysOfCode Laravel Challenge series. We are building the Blog project while following the Laravel 8 from Scratch series on Laracasts with the 100DaysOfCode challenge.

Lazy Eager Loading

In previous episodes, we fixed the N+1 problem on the blog overview page by Eager loading the Category and User models with the Post model as part of the query.

  • routes/web.php
Route::get('/', function () {
    return view('posts', [
        'posts' => Post::latest()->with("category", "author")->get()
    ]);
});

Now the N+1 problem is again there on the Author and Category pages.

The route definitions for these pages are as follows:

  • routes/web.php
Route::get('/categories/{category:slug}', function (Category $category) {
    return view('posts', [
        'posts' => $category->posts
    ]);
});

Route::get('/authors/{author:username}', function (User $author) {
    return view('posts', [
        'posts' => $author->posts
    ]);
});

So, here is the problem. How to load relationships on already-retrieved models like above? The solution is Lazy Eager Loading. Use the load() method with relation names passed as parameters.

  • routes/web.php
Route::get('/categories/{category:slug}', function (Category $category) {
    return view('posts', [
        'posts' => $category->posts->load("author", "category")
    ]);
});

Route::get('/authors/{author:username}', function (User $author) {
    return view('posts', [
        'posts' => $author->posts->load("author", "category")
    ]);
});

12 vs 4. The number of queries reduced from 12 to 4, a big improvement.

Eager Loading By Default

Another way is to always load the relationships whenever a model is retrieved. This can be accomplished by introducing a $with property to the model.

  • App\Models\Post
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $guarded = [];

    protected $with = ['category', 'author'];

    public function getRouteKeyName()
    {
        return 'slug';
    }

    public function category() {
        return $this->belongsTo(Category::class);
    }

    public function author() {
        return $this->belongsTo(User::class, "user_id");
    }
}

Now you can remove the Lazy Eager Loading load() methods from the route definitions.

  • route/web.php
Route::get('/categories/{category:slug}', function (Category $category) {
    return view('posts', [
        'posts' => $category->posts
    ]);
});

Route::get('/authors/{author:username}', function (User $author) {
    return view('posts', [
        'posts' => $author->posts
    ]);
});

Reload your blog overview page to verify the N+1 problem still doesn't exist.

Now every time a Post will be retrieved, the related User and Category models will be loaded by default. Let's test in tinker.

Boot up tinker php artisan tinker

>>> Post::first();
=> App\Models\Post {#4287
     id: 1,
     user_id: 1,
     category_id: 1,
     title: "Dignissimos animi aut consequatur aspernatur in libero labore.",
     slug: "similique-repellendus-id-est-et-illum",
     excerpt: "Vitae voluptatibus aut et id blanditiis.",
     body: "Ut dolor sit quia minima.",
     published_at: null,
     created_at: "2021-12-25 14:18:42",
     updated_at: "2021-12-25 14:18:42",
     category: App\Models\Category {#4249
       id: 1,
       title: "magnam",
       slug: "dolor",
       created_at: "2021-12-25 14:18:42",
       updated_at: "2021-12-25 14:18:42",
     },
     author: App\Models\User {#4511
       id: 1,
       name: "John Doe",
       username: "JohnDoe",
       email: "[email protected]",
       email_verified_at: "2021-12-25 14:18:42",
       #password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi"
,
       #remember_token: "qFYOFvkIyS",
       created_at: "2021-12-25 14:18:42",
       updated_at: "2021-12-25 14:18:42",
     },
   }
>>>

Notice that the Category and User models loaded by default with the Post model. If you would like to selectively remove one of these loaded related models, you can pass its name to the without() as:

>>> Post::without("category")->first();
=> App\Models\Post {#4515
     id: 1,
     user_id: 1,
     category_id: 1,
     title: "Dignissimos animi aut consequatur aspernatur in libero labore.",
     slug: "similique-repellendus-id-est-et-illum",
     excerpt: "Vitae voluptatibus aut et id blanditiis.",
     body: "Ut dolor sit quia minima.",
     published_at: null,
     created_at: "2021-12-25 14:18:42",
     updated_at: "2021-12-25 14:18:42",
     author: App\Models\User {#4514
       id: 1,
       name: "John Doe",
       username: "JohnDoe",
       email: "[email protected]",
       email_verified_at: "2021-12-25 14:18:42",
       #password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi"
,
       #remember_token: "qFYOFvkIyS",
       created_at: "2021-12-25 14:18:42",
       updated_at: "2021-12-25 14:18:42",
     },
   }
>>>

Notice that now the Category model is included but the User is not. Want to exclude the User too?

>>> Post::without("category", "author")->first();
=> App\Models\Post {#3564
     id: 1,
     user_id: 1,
     category_id: 1,
     title: "Dignissimos animi aut consequatur aspernatur in libero labore.",
     slug: "similique-repellendus-id-est-et-illum",
     excerpt: "Vitae voluptatibus aut et id blanditiis.",
     body: "Ut dolor sit quia minima.",
     published_at: null,
     created_at: "2021-12-25 14:18:42",
     updated_at: "2021-12-25 14:18:42",
   }
>>>

Lazy Eager Loading or Eager Loading By Default?

So, what is the better option? When to use which approach? You know the answer better. I mean it depends on the conditions under which you will use one approach or the other.

Lazy eager loading is useful when you are not sure in the first place if the relations will be used. Later at some point, you load load() the relations based on some condition.

On the other hand, if you are sure that the relations will always be needed whenever the main model will be referenced, then better to load them by default using the $with property as explained above.

Thank you for following along with me. Stay tuned for the next article. Comments and suggestions are always appreciated and welcome.

~ Happy Coding!

11