1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Laravel]想定外のN+1

Last updated at Posted at 2022-03-05

はじめに

業務で「こんな形で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モデルには以下のようにリレーションを張っている

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

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

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

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

<?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

いやいや、待て待て。

なぜN+1???

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

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

謎。。。

やったこと

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

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

原因

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

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

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

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

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

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

Middleware今回実装箇所 : userAttribute取得

Route

Controller : ここでもuserAttribute取得既にMiddlewareで同じクエリ発行済みなのでN+1と判断される

対応方法

じゃあ、どうするか。

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

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

なので、N+1のアラートを表示する laravel-query-detector の設定を変更し、アラートを表示させないようにした

具体的には laravel-query-detector をインストールした時に入ってくるconfig/querydetector.phpのexceptの箇所に以下記述

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

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

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

尚、 laravel-query-detector 本家のconfigのexceptは以下

最後に

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

すごく便利な laravel-query-detector ですが、ちょっとおせっかいな部分もあるのだな、と感じました。

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

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

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?