11
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.
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.
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",
}
>>>
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