'laravel strange N+1 query issue
I'm developing a website where the user is able to track movies and tv series by adding them to specific diaries, i have main diaries such as "Watched", "Favourite" and "To Watch" and custom diaries that the user is able to create. If a user moves a movie or tv series from the favourite to the to watch diaries, the movie or tv series should be first removed from the watched and then from the favourite diaries and finally added to the to watch diary and this is fine, but when i do the opposite, (from to watch diary to favourite and so, to watched) in the log i get:
[2022-03-03 12:42:07] local.INFO: Detected N+1 Query
[2022-03-03 12:42:07] local.INFO: Model: App\Models\Diary
Relation: App\Models\Movie
Num-Called: 2
Call-Stack:
#17 /app/Models/Diary.php:90
#18 /app/Traits/ScreenplayActions.php:41
#19 /app/Traits/ScreenplayActions.php:54
#20 /app/Traits/Screenplayability.php:131
[2022-03-03 12:42:07] local.INFO: Model: App\Models\Diary
Relation: App\Models\Series
Num-Called: 2
Call-Stack:
#17 /app/Models/Diary.php:90
#18 /app/Traits/ScreenplayActions.php:41
#19 /app/Traits/ScreenplayActions.php:54
#20 /app/Traits/Screenplayability.php:131
[2022-03-03 12:42:07] local.INFO: Model: App\Models\Diary
Relation: App\Models\Movie
Num-Called: 2
Call-Stack:
#17 /app/Models/Diary.php:111
#18 /app/Traits/ScreenplayActions.php:34
#19 /app/Traits/ScreenplayActions.php:56
#20 /app/Traits/ScreenplayActions.php:41
#21 /app/Traits/ScreenplayActions.php:54
#22 /app/Traits/Screenplayability.php:131
[2022-03-03 12:42:07] local.INFO: Model: App\Models\Diary
Relation: App\Models\Series
Num-Called: 2
Call-Stack:
#17 /app/Models/Diary.php:111
#18 /app/Traits/ScreenplayActions.php:34
#19 /app/Traits/ScreenplayActions.php:56
#20 /app/Traits/ScreenplayActions.php:41
#21 /app/Traits/ScreenplayActions.php:54
#22 /app/Traits/Screenplayability.php:131
I cut some parts of the log there are not so needed.
as you can see the N+1 problems happens starting at line 90 and 111 in the Diary model:
/**
* Get watched diary object.
*
* @return Diary
*/
public static function getWatched(): Diary
{
return self::watched()->first(); // LINE 90
}
/**
* Get watched diary builder.
*
* @param Builder $builder
* @return mixed
*/
public function scopeWatched(Builder $builder)
{
return $builder->where('name', self::WATCHED_DIARY_NAME)->main();
}
/**
* Get to watch diary object.
*
* @return Diary
*/
public static function getToWatch(): Diary
{
return self::toWatch()->first(); //LINE 111
}
/**
* Get to watch diary builder.
*
* @param Builder $builder
* @return Builder
*/
public function scopeToWatch(Builder $builder): Builder
{
return $builder->where('name', self::TO_WATCH_DIARY_NAME)->main();
}
/**
* Get main diaries builder.
*
* @param Builder $builder
* @return Builder
*/
public function scopeMain(Builder $builder): Builder
{
return $builder->where('isMain', true);
}
/**
* Get favourite diary object.
*
* @return Diary
*/
public static function getFavourite(): Diary
{
return self::favourite()->first();
}
/**
* Get favourite diary builder.
*
* @param Builder $builder
* @return Builder
*/
public function scopeFavourite(Builder $builder): Builder
{
return $builder->where('name', self::FAVOURITE_DIARY_NAME)->main();
}
this is the trait that both the Movie and Series models use to manage the data in the diaries:
namespace App\Traits;
use App\Classes\TMDBScraper;
use App\Models\Diary;
use App\Models\Movie;
use App\Models\Series;
use Illuminate\Database\Eloquent\Casts\Attribute;
trait ScreenplayActions
{
/**
* Get the screenplay runtime depending if the screenplay is a movie or a series
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function runtime(): Attribute
{
return Attribute::make(
get: function ($value) {
return $this instanceof Movie
? $value . ' ' . __('minutes')
: ($this instanceof Series
? $value . ' ' . __('minutes/episode')
: $value);
},
);
}
public function watch()
{
if ($this->isToBeWatched()) {
$this->removeFromDiary(Diary::getToWatch());
}
}
public function makeFavourite()
{
if (!$this->isWatched()) {
$this->addToDiary(Diary::getWatched());
}
}
public function toBeWatched()
{
$this->removeFromDiary(Diary::getWatched());
}
public function addToDiary(Diary $diary)
{
if (!$this->existsInDiary($diary)) {
if ($diary->isFavourite()) {
$this->makeFavourite();
} elseif ($diary->isWatched()) {
$this->watch();
} elseif ($diary->isToWatch()) {
$this->toBeWatched();
}
$diary->{$this->getTable()}()->attach($this->id);
session()->flash(
'message',
__('flash.screenplay_added', [
'screenplay_title' => $this->title,
'diary_name' => $diary->isMain ? __($diary->name) : $diary->name,
]),
);
}
}
public function isWatched()
{
return $this->existsInDiary(Diary::getWatched());
}
public function isFavourite()
{
return $this->existsInDiary(Diary::getFavourite());
}
public function isToBeWatched()
{
return $this->existsInDiary(Diary::getToWatch());
}
public function existsInDiary(Diary $diary)
{
return (bool) Diary::whereId($diary->id)
->whereHas($this->getTable(), function ($builder) {
$builder->where(self::getModelClassName() . "_id",'=', $this->id);
})
->count();
}
public function removeFromDiary(Diary $diary)
{
if ($diary->isWatched()) {
$this->diaries()->detach(Diary::getFavourite()->id);
}
$this->diaries()->detach($diary->id);
session()->flash('message', __('flash.screenplay_removed', ['screenplay_title' => $this->title]));
}
public static function getModelClassName()
{
return strtolower((new \ReflectionClass(self::class))->getShortName());
}
public static function firstOrTranslate(TMDBScraper $TMDBScraper, $id): Movie|Series|null
{
$screenplayClass = get_called_class();
$screenplayTranslations = $TMDBScraper->translate($id, new $screenplayClass());
if ($screenplayClass && !is_null($screenplayTranslations)) {
return self::firstOrCreate(compact('id'), $screenplayTranslations);
}
return null;
}
}
can anyone help me to fix this N+1 issue?
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
