[Laravel]想定外のN+1

# はじめに

業務で「こんな形でN+1が発生するんだな」と衝撃だったので、メモ

# 前提

## 環境
Laravel 8.5.0
PHP 8.0.2

## やりたいこと

いわゆる「垢BAN機能」を実装したい

※ちなみに、この機能自体は本題と関係ないです。

## テーブル構成

テーブルは以下の通り

※必要ない箇所は割愛

- Userテーブル

| カラム名 | 型 | 制御 | 説明 |
| --- | --- | --- | --- |
| id | int | PK | ユーザーID |
| name | string | not null | 名前 |
| ... | ... | ... | ... |
- UserAttributeテーブル

| カラム名 | 型 | 制御 | 説明 |
| --- | --- | --- | --- |
| id | int | PK | ユーザー詳細ID |
| user_id | string | FK | ユーザーID |
| ... | ... | ... | ... |
| blocked | tinyint | nullable | ブロックフラグ(1だとBANされているアカウント)  |
| ... | ... | ... | ... |

## 実装方法

事前に、UserモデルからuserAttributeモデルには以下のようにリレーションを張っている

```php
public function userAttribute()
{
    return $this->hasOne(UserAttribute::class);
}
```

そして、こちらのソースコードをほぼそのまま使って実装

アカウントのBANするか否かのチェックをミドルウェアで持たせてる

※実際はEnum使ったりしてますが、本題からそれるので割愛

https://dev.to/shanisingh03/how-to-ban-suspend-users-in-laravel-8-1gh3
```php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class CheckBlockedUser
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        if (Auth::check() && $user = Auth::user()) {
            $userAttribute = $user->userAttribute; // ※

            if ($userAttribute && $userAttribute->blocked == 1) {
                Auth::logout();
                $request->session()->regenerateToken();
                Log::notice('Block user from login status. '.$user->id);

                return redirect()->route('login')
                    ->withErrors(['error' => 'アカウントは凍結されています。']);
            }
        }

        return $next($request);
    }
}
```

チェックするのはUserAttributesのblockedプロパティにあるので、**※の箇所でuserからuserAttributesを取得してから、blockedへアクセスしている**

上記**太字部分**が本題。

これで、問題ないだろうと思い、動作テストをしてみたところ垢BAN機能は実装できていた。

# 起こったこと

ただ、正常ユーザー（ブロックされていないユーザー）でログインできるかもテストしておこうと思い、ログインをしてみる

すると、こいつが出てきた

※実行環境がlocalhostなのは気になさらないでください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780929/1f83e62f-bda0-909f-b2c4-5daf44eea8b4.png)


いやいや、待て待て。

なぜN+１？？？

User→UserAttributeって`$userAttribute = $user->userAttribute`で、1ユーザーしか取得してないやん。

しかも、ログイン後に遷移するページによってはN+1が発生するところと、発生しないところがあった

謎。。。

# やったこと

とはいえ、N+1は対処しなければいけないので、N+1が発生するところでサクッとLaravel-debugbarでクエリを確認する

https://github.com/barryvdh/laravel-debugbar

すると、原因が分かってきた

## 原因

原因は**Controlerでも同じように$user→userattributeへのアクセスをして同じSQLが実行されていた**ためだった。

実際にControllerで既に存在したソースコードが以下

```php
$user = User::find(Auth::id());
$userAttribute = $user->userAttribute; // ※ココでuserAttributeを取得すSQLが発行
```

他のN＋１が発生していたページのControllerを見ると同じような記述が見つかったので、これが原因だと断定

Laravelとしては既にMiddlewareで$userAttributeを取得しているのに、Controllerでも取得しているもんだから、N+1だと判断しちゃうんだな。。。

※N+1発生までのイメージは以下

```php
Middleware（今回実装箇所） : userAttribute取得
↓
Route
↓
Controller : ここでもuserAttribute取得→既にMiddlewareで同じクエリ発行済みなのでＮ＋１と判断される
```

# 対応方法

じゃあ、どうするか。

MiddelwareもControllerもどちらもAuth::idかAuth::userの取得でないと機能が成り立たないし、

Middlewareで取得した$userAttributeをControllerまで渡すのは現実的ではない。

なので、N+1のアラートを表示する **[laravel-query-detector](https://github.com/beyondcode/laravel-query-detector)** の設定を変更し、アラートを表示させないようにした

https://github.com/beyondcode/laravel-query-detector

具体的には **[laravel-query-detector](https://github.com/beyondcode/laravel-query-detector)** をインストールした時に入ってくるconfig/querydetector.phpのexceptの箇所に以下記述

```php
'except' => [
        User::class => [
            UserAttribute::class,
            'userAttribute',
        ],
]
```

UserとUserAttributeは１対１なので、N＋１は基本発生しない前提でこちらを設定

これで、N+1のアラートの発生は免れた

尚、 **[laravel-query-detector](https://github.com/beyondcode/laravel-query-detector)** 本家のconfigのexceptは以下

https://github.com/beyondcode/laravel-query-detector/blob/master/config/config.php#L23

# 最後に

まさか、MiddlewareとControllerで同じクエリが発行されているとは気づかないし、これをN＋１と判断されるとも知らなかった…

すごく便利な **[laravel-query-detector](https://github.com/beyondcode/laravel-query-detector)** ですが、ちょっとおせっかいな部分もあるのだな、と感じました。

同時に、こんな時にexceptの機能があるのはありがたいなと感じました。

最後までお読みいただきありがとうございました。