👨‍👩‍👧‍👦

LaravelでN + 1を検知した際にログファイルに出力する

2023/10/29に公開

はじめに

今回Laravel Query Detectorというパッケージを使ってN+1を検知した際にログファイルに出力してみます。
https://github.com/beyondcode/laravel-query-detector

環境

  • PHP 8.2.12
  • Laravel 10.29.0

事前準備

今回の検証に使うテーブルやEloquentモデルクラスを事前に作成しておきます。

テーブル作成

デフォルトで用意されているusersテーブルと1対多で紐づくテーブルを作成

database/migrations/2023_10_28_153502_create_tasks_table.php
database/migrations/2023_10_28_153502_create_tasks_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->string('title');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};

Eloquentモデルクラス

app/Models/Task.php
app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;

    public function user(): BelongsTo
    {
      return $this->belongsTo(User::class);
    }
}
database/factories/TaskFactory.php
database/factories/TaskFactory.php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Task>
 */
class TaskFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'title' => Str::random(10),
        ];
    }
}

デフォルトで用意されているUserモデルにリレーションの設定を追加しておきます。

app/Models/User.php
use Illuminate\Database\Eloquent\Relations\HasMany;

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

データ作成

tinkerを使って下記を実行し、usersテーブルと紐づくtasksテーブルのデータを作って起きます。

User::factory()
    ->has(Task::factory()->count(10))
    ->count(10)
    ->create();

N+1が起きるコード

welcomeページのルートに次のコードを仕込んでみます。

routes/web.php
Route::get('/', function () {
    $tasks = Task::all();
    foreach ($tasks as $task) {
        $task->user->id;
    }
    return view('welcome');
});

使い方

インストール

composer require beyondcode/laravel-query-detector --dev

この記事で使うバージョンは1.7.0です。

$ composer show | grep beyondcode/laravel-query-detector   
beyondcode/laravel-query-detector  1.7.0    Laravel N+1 Query Detector

インストール後、設定ファイルをコピーしておきます。

php artisan vendor:publish --provider="BeyondCode\QueryDetector\QueryDetectorServiceProvider"

ログファイルに出力する設定

デフォルトでログファイルに出力する設定になっているかもしれないですが、
なっていない場合はコピーした設定ファイル(config/querydetector.php)の内容を次に書き換えます。

config/querydetector.php
    'output' => [
        \BeyondCode\QueryDetector\Outputs\Log::class, // <- 追記する
    ]

また、.envファイルのAPP_DEBUGtrueを設定しておく必要があります。

お試し

事前準備で用意したwelcomeページにブラウザでアクセスすれば
storage/logs/laravel-yyyy-mm-dd.logファイルができていると思いますので
中身を確認すると次のような内容が出力していると思います。

storage/logs/laravel-yyyy-mm-dd.log
[yyyy-mm-dd XX:XX:XX] local.INFO: Detected N+1 Query  
[yyyy-mm-dd XX:XX:XX] local.INFO: Model: App\Models\Task
Relation: user
Num-Called: 100
Call-Stack:
〜〜〜 以降はスタックトレースの内容 〜〜〜

まとめ

知らない間にN+1のコードを作成している場合があったりするので、
ログファイルに出力しておけばCIでユニットテストを実行してN+1の場所を探し出せたりするから便利かなって思います。

また、今回は試していないですが、ブラウザのコンソールに出力すれば動作確認中でも見つけることができるので
設定ファイルのoutput配列に\BeyondCode\QueryDetector\Outputs\Console::classを追記しておくのもいいかなって思います。

GitHubで編集を提案

Discussion