Disable eloquent lazy loading during development
In the next version of Laravel 8, you can strictly disable lazy loading, which results in an exception:
Strict to avoid N + 1 query problems? @themsaidThe recent contribution of the structure to the framework allows you to completely disable deferred loading (an exception will be thrown) …
Can deactivate it only in case of non-production so that the production does not crash if we slip! ð
Shipped next week! pic.twitter.com/5Q9YpCLRze
– Taylor Otwell ðª (@taylorotwell) May 19, 2021
Preventing lazy loading in development can help you find N + 1 bugs earlier in the development process. The Laravel ecosystem has different tools to identify N + 1 requests. However, this approach brings the problem to the fore by throwing an exception.
Demo
Let’s go over this feature very quickly by creating a development version of the framework. 8.x
branch because this feature is not yet available at the time of writing. Once published, you will have this feature without going to the last one 8.x
plugged.
Install
Start by creating a new app:
laravel new strict-lazy-demo
Then we will update the laravel/framework
version in composer.json
to make sure we have this functionality (if you try it before the next version) by adjusting the version to 8.x-dev
:
{
"require": {
"laravel/framework": "8.x-dev"
}
}
Then run composer update
to make sure you get the latest code for this branch:
composer update laravel/framework
At this point, you need to configure your preferred database. I like to run a local MySQL instance using Laravel’s default settings root
user without password. I find it convenient to use the default .env
values ââlocally to start quickly without any configuration.
mysql -uroot -e"create database strict_lazy_demo"
Once you have configured the database of your choice, make sure you can migrate:
php artisan migrate:fresh
Demonstration data
We will create a Post
model and define a one-to-many
relation of the User
model to demonstrate this functionality. We will start by creating the Post
template 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(AppModelsUser::class);
$table->string('title');
$table->longText('body');
$table->timestamps();
});
Then set your PostFactory
definition method based on the diagram above:
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'user_id' => AppModelsUser::factory(),
'title' => $this->faker->sentence(),
'body' => implode("nn", $this->faker->paragraphs(rand(2,5))),
];
}
Finally, open the DatabaseSeeder
file and add the following in the run()
method:
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
AppModelsUser::factory()
->has(AppModelsPost::factory()->count(3))
->create()
;
}
Pairing models and preventing lazy loading
Now that we have created the migration, planter, and model, we are ready to associate a user with the Post model to demonstrate this functionality.
Add the following method to the User
template to give the user an association with the messages:
// app/Models/User.php
/**
* @return IlluminateDatabaseEloquentRelationsHasMany
*/
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 use tinker to inspect our predefined data and relationships:
php craftsman tinker >>> $ user = User :: first () => App Models User {# 4091 id: 1, name: "Nedra Hayes", email: "[email protected]", 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: [
AppModelsPost {#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 = AppModelsUser::first()
=> AppModelsUser {#3685
id: 1,
name: "Nedra Hayes",
email: "[email protected]",
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
IlluminateDatabaseLazyLoadingViolationException with message
'Attempted to lazy load [posts] on model [AppModelsUser] but lazy loading is disabled. '
If you want to see what happens if you use lazy loading to a view file, change the default route as follows:
Route::get("https://laravel-news.com/", function () {
return view('welcome', [
'user' => AppModelsUser::first()
]);
});
Then add the following somewhere in the welcome.blade.php
deposit:
Posts
@foreach($user->posts as $post)
{{ $post->title }}
{{ $post->body }}
@endforeach
If you load your app through Valet or artisan serve
, you should see something like the following error page:
Although you get exceptions during development, accidental deployment of code that triggers a lazy load will continue to work as long as you set up environment verification 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. Many thanks to Mohamed Said, the contributors, and of course Taylor Otwell for adding the polish to disable the lazy loading conditionally.