Find N+1 problems instantly by disabling lazy loading

Published on by

Find N+1 problems instantly by disabling lazy loading image

In the next release of Laravel 8, you can strictly disable lazy loading entirely, resulting in an exception:

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.

Demo

Let’s walk through this feature real quick by spinning up a development version of the framework 8.x branch since this feature is not out yet at the time of writing. Once released, you will have this feature without switching to the latest 8.x branch.

Setup

First, create a new application:

laravel new strict-lazy-demo

Next, we’ll update the laravel/framework version in composer.json to make sure we have this feature (if you’re trying it out before the next release) by adjusting the version to 8.x-dev:

{
"require": {
"laravel/framework": "8.x-dev"
}
}

Next, run composer update to make sure you get the latest version of the code for this branch:

composer update laravel/framework

At this point, you should set up your preferred database. I like running a local MySQL instance using Laravel’s defaults of using the root user without a password. I find it convenient to use the default .env values locally to get started quickly without any configuration.

mysql -uroot -e"create database strict_lazy_demo"

Once you configure your database of choice, make sure you can migrate:

php artisan migrate:fresh

Demo Data

We’ll create a Post model and define a one-to-many relationship from the User model to demonstrate this feature. We’ll start by creating the Post model and accompanying files:

# Create a model with migration and factory
php artisan make:model -mf Post

First, let’s define our Post migration and factory configuration:

// Your filename will differ based on when you create the file.
// 2021_05_21_000013_create_posts_table.php
 
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(\App\Models\User::class);
$table->string('title');
$table->longText('body');
$table->timestamps();
});

Next, define your PostFactory definition method based on the above schema:

/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'user_id' => \App\Models\User::factory(),
'title' => $this->faker->sentence(),
'body' => implode("\n\n", $this->faker->paragraphs(rand(2,5))),
];
}

Finally, open up the DatabaseSeeder file and add the following in the run() method:

/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
\App\Models\User::factory()
->has(\App\Models\Post::factory()->count(3))
->create()
;
}

Associating Models and Prevent Lazy Loading

Now that we have the migration, seeder, and model created, we are ready to associate a User with the Post model to demo this feature.

Add the following method to the User model to give the user an association with Posts:

// app/Models/User.php
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function posts()
{
return $this->hasMany(Post::class);
}

With that in place, we can migrate and seed the database:

php artisan migrate:fresh --seed

If all went well, we should see something like the following in the console:

We can now using tinker to inspect our seeded data and relationship:

php artisan tinker
 
>>> $user = User::first()
=> App\Models\User {#4091
id: 1,
name: "Nedra Hayes",
email: "bruen.marc@example.com",
email_verified_at: "2021-05-21 00:35:59",
created_at: "2021-05-21 00:35:59",
updated_at: "2021-05-21 00:35:59",
}
>>> $user->posts
=> Illuminate\Database\Eloquent\Collection {#3686
all: [
App\Models\Post {#3369
id: 1,
...

The $user->posts property actually calls the database, thus is "lazy" but is not optimized. The convenience of lazy-loading is nice, but it can come with heavy performance burdens in the long-term.

Disabling Lazy Loading

Now that we have the models set up, we can disable lazy loading across our application. You’d likely want to only disable in non-production environments, which is easy to achieve! Open up the AppServiceProvider class and add the following to the boot() method:

// app/Providers/AppServiceProvider.php
 
public function boot()
{
Model::preventLazyLoading(! app()->isProduction());
}

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

php artisan tinker
 
>>> $user = \App\Models\User::first()
=> App\Models\User {#3685
id: 1,
name: "Nedra Hayes",
email: "bruen.marc@example.com",
email_verified_at: "2021-05-21 00:35:59",
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "jHSxFGKOdw",
created_at: "2021-05-21 00:35:59",
updated_at: "2021-05-21 00:35:59",
}
>>> $user->posts
Illuminate\Database\LazyLoadingViolationException with message
'Attempted to lazy load [posts] on model [App\Models\User] but lazy loading is disabled.'

If you want to visualize what happens if you use lazy loading in a view file, modify the default route as follows:

Route::get('/', function () {
return view('welcome', [
'user' => \App\Models\User::first()
]);
});

Next, add the following somewhere in the welcome.blade.php file:

<h2>Posts</h2>
@foreach($user->posts as $post)
<h3>{{ $post->title }}</h3>
<p>
{{ $post->body }}
</p>
@endforeach

If you load up your application through Valet or artisan serve, you should see something like the following error page:

Though you’ll get exceptions during development, accidentally deploying code that triggers lazy-loading will continue to work as long as you set environment checking correctly in the service provider.

Learn More

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.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article