Avoiding the N+1 problem in Laravel

Eduar Bastidas • September 2, 2021

tips refactoring

Let's do some piggybacking on our code from Tip #1.

1<?php
2 
3public function usersAverageScore() {
4 $participants = $this->participants;
5 
6 return $participants->filter(function ($participant) {
7 return $participant->isActive();
8 })->average(function ($participant) {
9 return $participant->user->averageMessages();
10 });
11}

Note how we fetch the User model for a given participant in the average function. This is highly problematic, because we're doing an additional SQL query "behind the scenes" by loading many users models one at a time.

A better solution for this would be to eager load them on our first query. When we do that, we can reduce the number considerably, instead of having N+1 queries (hence the name of that dreaded issue).

It is easy to do this with the with method of eloquent. Let's refactor the code above:

1<?php
2 
3public function usersAverageScore() {
4 $participants = $this->participants()->with('user')->get();
5 
6 return $participants->filter(function ($participant) {
7 return $participant->isActive();
8 })->average(function ($participant) {
9 return $participant->user->averageMessages();
10 });
11}

Now, whenever we call $participant->user->averageMessages(), the user model related to the participant was already cached during our first call ($this->participants()).

(By the way, there's still one non-optimized call related to this tip in the code above - can you spot it? Leave it in the comments).

This is a problem that has been discussed by many Laravel developers for some time now. However I will give another example here so that you can imagine another situation.

Preventing lazy loading in development can help you catch N+1 bugs earlier on in the development process. The Laravel ecosystem has various tools to identify N+1 queries. However, this approach brings the issue front-and-center by throwing an exception.

Recently this year Mohamed Said has contributed a new feature that avoids this behavior in the first place and prevents this error from occurring in an application under development.

Open up the AppServiceProvider class and add the following to the boot() method:

1// app/Providers/AppServiceProvider.php
2 
3public function boot()
4{
5 Model::preventLazyLoading(! app()->isProduction());
6}

This will only prevent this behavior in non-productive environments.

If you run a php artisan tinker session, you should get an exception for a lazy loading violation:

1>>> $participants = $this->participants;
2>>> $participants[0]->user
3Illuminate\Database\LazyLoadingViolationException with message
4'Attempted to lazy load [user] on model [App\Models\Participant] but lazy loading is disabled.'

You can learn how this feature was implemented: 8.x Add eloquent strict loading mode - Pull Request #37363. Huge thanks to Mohamed Said, contributors, and of course Taylor Otwell for adding the polish to disable lazy loading conditionally.