Eloquent のＮ＋１問題には Eeger ロード

## 前回のあらすじ


ユーザーが所属する会社を横断したプロジェクトに属している場合に、ログインユーザーと関わりのあるプロジェクトの会社名すべて取得するリレーションを [紹介した](https://qiita.com/mikomey/items/705834f47dcf4d3cbd1e)。
```php
$user = User::find(1);
$names = $user->projects->pluck('users')
    ->flatten()->pluck('company')
    ->pluck('name')->unique();
```
しかし、このままではループの中でクエリが発行される「**Ｎ＋１問題**」が生じ、クエリ数がとんでもないことになってしまう。
このことについて、実際にデータを作成して検証してみる。

テスト用のデータ作成は[こちらを参照](https://qiita.com/mikomey/items/ea3f4126962839581a4f)。


## tinker でリレーションを試す

今回はクエリを確認するために tinker を起動したらすぐに `DB::enableQueryLog()` を実行する。

```php
$ php artisan tinker
Psy Shell v0.11.4 (PHP 7.4.11 — cli) by Justin Hileman
>>> DB::enableQueryLog()
=> null
```
これ以降の操作で発生したクエリは `DB::getQueryLog()` で確認することができる。ユーザーを１件指定して、リレーションで紐づく関連会社の名前を取得してみる。

```php
>>> $user = User::find(1);
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
=> App\Models\User {#3588
     id: 1,
     name: "鈴木 修平",
     email: "cyamada@example.com",
     company_id: 1,
     email_verified_at: "2022-06-19 05:12:17",
     #password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
     #remember_token: "14rSAboFzn",
     created_at: "2022-06-19 05:12:17",
     updated_at: "2022-06-19 05:12:17",
   }

>>> $names = $user->projects->pluck('users')->flatten()->pluck('company')->pluck('name')->unique();
=> Illuminate\Support\Collection {#3605
     all: [
       0 => "有限会社 喜嶋",
       3 => "有限会社 渚",
       5 => "有限会社 藤本",
       10 => "株式会社 伊藤",
       13 => "株式会社 吉本",
       15 => "有限会社 村山",
       21 => "有限会社 佐々木",
       27 => "有限会社 山岸",
       30 => "株式会社 山田",
     ],
   }

>>> count(DB::getQueryLog())
=> 234

>>> collect(DB::getQueryLog())->sum('time')
=> 41.26
```

Ｎ＋１が発生してるのが明らかなクエリ数だ。レコードの件数が増えれば、クエリ数も実行時間も比例して増大していくだろう。
どんなクエリが発行されたかログを確認してみる。

```php

>>> DB::getQueryLog()
=> [
     [
       "query" => "select * from `users` where `users`.`id` = ? limit 1",
       "bindings" => [
         1,
       ],
       "time" => 1.55,
     ],
     [
       "query" => "select `projects`.*, `project_user`.`user_id` as `pivot_user_id`, `project_user`.`project_id` as `pivot_project_id` from `projects` inne
r join `project_user` on `projects`.`id` = `project_user`.`project_id` where `project_user`.`user_id` = ?",
       "bindings" => [
         1,
       ],
       "time" => 0.79,
     ],
     [
       "query" => "select `users`.*, `project_user`.`project_id` as `pivot_project_id`, `project_user`.`user_id` as `pivot_user_id` from `users` inner join
 `project_user` on `users`.`id` = `project_user`.`user_id` where `project_user`.`project_id` = ?",
       "bindings" => [
         2,
       ],
       "time" => 0.51,
     ],
     [
       "query" => "select `users`.*, `project_user`.`project_id` as `pivot_project_id`, `project_user`.`user_id` as `pivot_user_id` from `users` inner join
 `project_user` on `users`.`id` = `project_user`.`user_id` where `project_user`.`project_id` = ?",
       "bindings" => [
         4,
       ],
       "time" => 0.34,
     ],
      ～～（省略）～～
     [
       "query" => "select `users`.*, `project_user`.`project_id` as `pivot_project_id`, `project_user`.`user_id` as `pivot_user_id` from `users` inner join
 `project_user` on `users`.`id` = `project_user`.`user_id` where `project_user`.`project_id` = ?",
       "bindings" => [
         9,
       ],
       "time" => 0.31,
     ],
     [
       "query" => "select * from `companies` where `companies`.`id` = ? limit 1",
       "bindings" => [
         1,
       ],
       "time" => 0.27,
     ],
     [
       "query" => "select * from `companies` where `companies`.`id` = ? limit 1",
       "bindings" => [
         1,
       ],
       "time" => 0.16,
     ],
      ～～（以下略）～～
```

いくつかのクリエがくり返されているが、ログを **from で検索** してみると理解しやすくなる。順に……

  1. `from users` が１回
  2. `from projects` が１回
  3. `from users` がいくつか
  5. `from companies` が延々と続く

そこで、ユーザーを起点して会社までのリレーション名（テーブル名ではない）をドット記法でつなげた `projects.users.company` を load メソッドで指定することにする（**Eager ロード**）。

クエリログを初期化するために tinker を立ち上げなおす。

```php
$ php artisan tinker
Psy Shell v0.11.4 (PHP 7.4.11 — cli) by Justin Hileman
>>> DB::enableQueryLog()
=> null

>>> $user = User::find(1);
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
=> App\Models\User {#3590
     ～～（省略）～～
   }

>>> $user->load('projects.users.company')
```

load コマンドの実行結果がズラズラ表示されるがここでは省略する。
スクロールが止まったら、いよいよリレーションを取得。


```php
>>> $names = $user->projects->pluck('users')->flatten()->pluck('company')->pluck('name')->unique();
=> Illuminate\Support\Collection {#4358
     all: [
       0 => "有限会社 喜嶋",
       3 => "有限会社 渚",
       5 => "有限会社 藤本",
       10 => "株式会社 伊藤",
       13 => "株式会社 吉本",
       15 => "有限会社 村山",
       21 => "有限会社 佐々木",
       27 => "有限会社 山岸",
       30 => "株式会社 山田",
     ],
   }

>>> count(DB::getQueryLog())
=> 4

>>> collect(DB::getQueryLog())->sum('time')
=> 3.67
```
結果は同じなのに、クエリ数はたったの４つ！！
この４つとは、先に調べたように from したテーブルの数だ。クエリの内容を確認みると、必要な値を先に取得して一度ずつ取得しているのがわかる。

```php
>>> DB::getQueryLog()
=> [
     [
       "query" => "select * from `users` where `users`.`id` = ? limit 1",
       "bindings" => [
         1,
       ],
       "time" => 1.53,
     ],
     [
       "query" => "select `projects`.*, `project_user`.`user_id` as `pivot_user_id`, `project_user`.`project_id` as `pivot_project_id` from `projects` inner join `project_user` on `projects`.`id` = `project_user`.`project_id` where `project_user`.`user_id` in (1)",
       "bindings" => [],
       "time" => 0.72,
     ],
     [
       "query" => "select `users`.*, `project_user`.`project_id` as `pivot_project_id`, `project_user`.`user_id` as `pivot_user_id` from `users` inner join `project_user` on `users`.`id` = `project_user`.`user_id` where `project_user`.`project_id` in (2, 4, 5, 6, 7, 8, 9)",
       "bindings" => [],
       "time" => 0.96,
     ],
     [
       "query" => "select * from `companies` where `companies`.`id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)",
       "bindings" => [],
       "time" => 0.46,
     ],
   ]
```
レコード数が増えてもクエリ数は変わらなくなった。



## Eager ロードは load と with

今回はユーザーを１件取得後に Eager ロードするため **load** を使用したが、ユーザー一覧的に取得する場合には **with** を用いる（load のほうが「遅延 Eager ロード」と呼ばれる）。
複数のリレーションを Eager ロードしなければならないときには、load も with も配列で指定できる。次の例では「所属会社」のところでもＮ＋１が起きるので、配列で指定を追加してる。

*Controller*
```php
$users  = User::with(['company', 'projects.users.company'])->get();
```
*Blade テンプレート*
```html
<tr>
  <th>ID</th>
  <th>氏名</th>
  <th>所属会社</th>
  <th>関連会社</th>
</tr>
@foreach ($users as $user)
  <tr>
     <td>{{ $user->id }}</td>
     <td>{{ $user->name }}</td>
     <td>{{ $user->company->name }}</td>
     <td>{{ $user->projects->pluck('users')->flatten()->pluck('company')->pluck('name')->unique()->join('、') }}</td>
  </tr>
@endofreach
```

#### with 指定で思考停止しない

モデルに記述されたリレーションをなんでもかんでも with 指定すればよい……とか、なんだかよくわからないけど with は指定しなければならない……等と思考停止はしない。**with は書かなくても動く** のであり、使いもしないリレーションを指定すればかえって無駄なクエリが実行されるものだ。

- **最初は with を（無駄に）つけず** にプログラムを書き、出力結果の正しさを優先する。
- 画面表示（または API 出力）が確定したら [Lravel Telescope](https://readouble.com/laravel/8.x/ja/telescope.html) でクエリログを調べる。
  - Blade 画面表示なら [laravel-debugbar](https://github.com/barryvdh/laravel-debugbar) でもOK。
- クエリ数を確認しながら with に指定するリレーションを追加する。