📌

【laravel】N+1問題に気をつけろ

2024/08/17に公開

はじめに

アプリ開発中にN+1問題について度々指摘されてしまったので頭の中の整理と自分への戒めを込めて投稿

N+1を起こしていたコード

class Project extends Model
{
    // 略

    public function team()
    {
        return $this->belongsTo(Team::class);
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function tasks()
    {
        return $this->hasMany(Task::class);
    }

   // 略
}

上記のProject Modelのリレーションを以下のように使用

 public static function getGanttData(User $user): array
    {
        $current_team = $user->selectedTeam;
        $projects = $current_team->projects;

        $ganttData = [];

        foreach ($projects as $project) {
            $projectData = static::processProject($project);
            $ganttData[] = $projectData;
        }

        return $ganttData;
    }

    private static function processProject($project): array
    {
        $tasks = $project->tasks->sortBy('start_date'); 

        $projectData = [
            'id' => "project-{$project->id}",
            'name' => $project->name,
            'start' => '',
            'end' => '',
            'progress' => 0,
            'dependencies' => null,
            'user_id' => $project->user_id,
            'user_name' => $project->user->name, 
        ];

        $processedTasks = static::processTasks($tasks, $project);

        if ($processedTasks->isNotEmpty()) {
            $projectData['start'] = $processedTasks->min('start');
            $projectData['end'] = $processedTasks->max('end');
        }

        return array_merge($projectData, ['tasks' => $processedTasks->toArray()]);
    }

N+1問題を起こしている箇所

 private static function processProject($project): array
    {
        $tasks = $project->tasks->sortBy('start_date');
        // 略

↑それぞれの$projectが持っているtasksを取得しようとしている

            //略
            'dependencies' => null,
            'user_id' => $project->user_id,
            'user_name' => $project->user->name, 
        ];

↑同じくそれぞれの$projectが持っているuserを取得しようとしている

このように、1回のクエリ発行でN件のレコードを取得し、それぞれN件のレコードが持っているリレーション先のテーブルのレコードを取得しようとする(N回のクエリ発行)とN+1問題が起こる。

解決手段

イーガーローディングを利用する
laravelではwith()メソッドを使用してイーガーローディングを実装することができる

具体的な修正コード

$projects = $current_team->projects()->with(['tasks' => function($query) {
    $query->orderBy('start_date');
}, 'user'])->get();

with()を使用してプロジェクト、タスク、およびユーザー情報を一度のクエリでプリロードすることで後続の

private static function processProject($project): array
{
    $tasks = $project->tasks;
    // 略

'user' => $project->name,

でクエリを発行することなく処理することができる。

N+1問題に気付くために

  1. ループ内でのデータベースクエリ:
    コード内でループを使用している箇所、特にコレクションや配列をイテレートしている部分で、各イテレーション内でデータベースクエリが実行されていないか確認する。

  2. リレーション参照の方法:
    モデル間のリレーションを参照する際、特に$model->relationのような形式で直接アクセスしている箇所に注意。

  3. ツールの使用:
    laravel Debugbarのようなデバッガーツールを使用してどんなクエリが発行されているかチェックする

参考記事

https://takeru232423.hatenablog.com/entry/2022/03/14/143846

Discussion