[Laravel]「これN＋１じゃね？」ってなりたい

# はじめに

最近よく知らず知らずのうちにN+1に入り込んでしまい、その解消工数が発生する

これが地味に頻度が多く、根本的にN+1を発生せずにコードを書いていきたいと思った

結局はN＋１はクエリが発行されるから起こることなので、LaravelのどのタイミングでN+1が発生するのか、を抑えれば解決するのではないかと思った

なので、頻繁に使うクエリを発行するメソッド（クエリビルダメソッド）をピックアップ

# メソッド一覧

## get()

- クエリビルダに対して使うことで、クエリを発行しDBで取得した値をコレクションに格納する
- get()の定義元はたくさんあるが、こんな感じでHasOne経由で取得してきたときの定義元を確認

```php
$users = User::with('userAttribute')->get();
```

```vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php
public function get($columns = ['*'])
    {
        $builder = $this->applyScopes();

        // If we actually found models we will also eager load any relationships that
        // have been specified as needing to be eager loaded, which will solve the
        // n+1 query issue for the developers to avoid running a lot of queries.
        if (count($models = $builder->getModels($columns)) > 0) {
            $models = $builder->eagerLoadRelations($models);
        }

        return $builder->getModel()->newCollection($models);
    }
```

1個づつ見ていく

### ①クエリの発行
```vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php
        if (count($models = $builder->getModels($columns)) > 0) {
            $models = $builder->eagerLoadRelations($models);
        }
```

- ここでは、`$columns = ['*']`を引数にとったgetModelsメソッドがクエリを発行してそうなので見て見る

```vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php
public function getModels($columns = ['*'])
    {
        return $this->model->hydrate(
            $this->query->get($columns)->all()
        )->all();
    }
```

- さらに`$columns = ['*']`を引数にとったgetがあり、これはBuider.phpのgetにつながっている


```vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php
public function get($columns = ['*'])
    {
        return collect($this->onceWithColumns(Arr::wrap($columns), function () {
            return $this->processor->processSelect($this, $this->runSelect());
        }));
    }
```

- いかにもクエリ発行してそうな`runSelect`メソッドを確認

```vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php

/**
* Run the query as a "select" statement against the connection.
*
* @return array
*/
protected function runSelect()
    {
        return $this->connection->select(
            $this->toSql(), $this->getBindings(), ! $this->useWritePdo
        );
    }
```

- メソッドのDocに書いてある通り、ここでSELECT文のSQLが発行されるということ

### ②取得結果をコレクションで返却

- 基本的に、ビルダで取得できたモデル（コレクションに変換前）が1以上であればif文に入る
- eagerLoadRelationsは名前の通りモデルごとにeagerloadをしてリレーションを張り、出来上がった複数モデルを配列として返す


```vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php
public function eagerLoadRelations(array $models)
    {
        foreach ($this->eagerLoad as $name => $constraints) {
            // For nested eager loads we'll skip loading them here and they will be set as an
            // eager load on the query to retrieve the relation so that they will be eager
            // loaded on that query, because that is where they get hydrated as models.
            if (strpos($name, '.') === false) {
                $models = $this->eagerLoadRelation($models, $name, $constraints);
            }
        }

        return $models;
    }
```

そして、配列で取得した複数モデルをコレクションに変換して返している

```php
        return $builder->getModel()->newCollection($models);
```


## all()

- 結論、上記のget()を内部で利用している
- よく使われるModelに対してのall()

```php
$users = User::all();
```

- 定義元は以下

```vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php
/**
     * Get all of the models from the database.
     *
     * @param  array|mixed  $columns
     * @return \Illuminate\Database\Eloquent\Collection|static[]
     */
    public static function all($columns = ['*'])
    {
        return static::query()->get(
            is_array($columns) ? $columns : func_get_args()
        );
    }
```

- ここで使っているget()は先ほどの\Eloquent\Builder.phpのget()なので、最終的にはrunSelectメソッドしてクエリ発行

## first()

例えば、以下のようなソースコード

```php
$users = User::whereBetween('id', [1, 10])->first();
```

ここでのfirst()は****PHP Intelephense****によるジャンプができないが、少なくともdumpするとデバック結果が表示されたので、以下を通る

```vendor\laravel\framework\src\Illuminate\Database\Concerns\BuildsQueries.php
/**
     * Execute the query and get the first result.
     *
     * @param  array|string  $columns
     * @return \Illuminate\Database\Eloquent\Model|object|static|null
     */
    public function first($columns = ['*'])
    {
        return $this->take(1)->get($columns)->first();
    }
```

- Docを見てわかる通り、クエリを発行しているんだな、と分かったが、もうちょい深追いしてみる
- ここでのgetも****PHP Intelephense****でジャンプできなかったが、dumpでデバッグすると上述の\Eloquent\Builder.phpのget()に入っていることが分かったので、最終的にはrunSelectメソッドでクエリを発行している
- getとかと一緒で、カラム名を指定できることは知らなかった…
- firstOrFail()とかも一緒

## find()

例えば以下のようなソースコード

```php
$users = User::whereBetween('id', [1, 10])->find(4);
```

ここのfindはvender配下の\Eloquent\Builder.phpを通っていた

```vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php
public function find($id, $columns = ['*'])
    {
        if (is_array($id) || $id instanceof Arrayable) {
            return $this->findMany($id, $columns);
        }

        return $this->whereKey($id)->first($columns);
    }
```

- ここで使っているfirstは上述のvender配下にある\Concerns\BuildsQueries.phpのfirstと同じなので、結局はget()→runSelect()につながる
- 第１引数を配列で渡せること、第２引数にカラム名を指定できることは知らなかったです…
- FindOrFail()とかも一緒

## HasOneなどのリレーションメソッド

[https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading](https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading)

- 無意識的に使ってたけど、コレで結構詰まった
    - ここでクエリ発行しているとは思わなかった

### サンプルコード
例えばこんな感じでModel、Controller or Middleware、Viewを記述する

↓Model

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

↓Contoroller or Middleware

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;

class EagerLoadingController extends Controller
{
    public function index() {
        $users = User::all(); // ※1回目クエリ発行！！
        $data = [
            'users' => $users,
        ];
        return view('eager_loading.index', $data);
    }
}
```

↓View

```php
<table>
    <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
    </tr>
    @foreach($users as $user)
    <tr>
        <td>{{$user->id}}</td>
        <td>{{$user->name}}</td>
        <td>{{$user->userAttribute->age}}</td> // ※n回（レコード）分のクエリが発行！！
    </tr>
    @endforeach
</table>
```

これでN+1が発生する、典型的な例。発生しているクエリはたくさん。

```php

select * from `users`; // +1のクエリ
// 以降はn回分のクエリ
select * from `user_attributes` where `user_attributes`.`user_id` = 1 and `user_attributes`.`user_id` is not null limit 1
select * from `user_attributes` where `user_attributes`.`user_id` = 2 and `user_attributes`.`user_id` is not null limit 1
select * from `user_attributes` where `user_attributes`.`user_id` = 3 and `user_attributes`.`user_id` is not null limit 1
...
...
select * from `user_attributes` where `user_attributes`.`user_id` = 20 and `user_attributes`.`user_id` is not null limit 1
```

では、Viewで{{$user->userAttribute->age}}のようにuseAttribureにアクセスしたときに、どこでクエリが発行されているか。

これについては正解が見つからなかった。

とは言え、冷静に考えれば、１対１ではなく、１対多のリレーション貼っているときに

↓Model

```php
public function orders() {
        return $this->hasMany(Order::class);
}
```

以下のようにordersにアクセスすると、戻り値はCollectionである。

↓Controller

```php
// １個のUserモデル取得
$user = User::find($id)
// リレーション先のordersにアクセス。出力はCollection
$orders = $user->orders
```
１対多（hasManyとか）も１対１（hasOneとか）も内部的には同じことをしているという前提に立つと、上記で「Conllectionが取得できている」ってことは、内部的には上述のvender配下にある\Eloquent\Builder.phpのget()使うのと同じなので、クエリが発行されていることも納得できる。

# おわりに

- 結局はこれらのメソッドでコレクションにしてからデータいじるわけなので、十分かなと思った
- こんな感じでクエリがどの行で発行されているか、がわかれば「ここN+1じゃね？」ってソースコード見ただけで気づけそうだ
- ただ、リレーションメソッド（hasOneなど）を使ってリレーションを張った時に

```php
// １個のUserモデル取得
$user = User::find($id)
// リレーション先のordersにアクセス。出力はCollection
$orders = $user->orders
```

こんな感じでリレーション先にアクセスした際のクエリの発行場所が特定できなかったので、今後必要に応じて調べる。
（hasOneメソッドを追ったんですが、自分の力不足で見つけられませんでした…）

- あと、別でN+1関連でつまづいた事例があったのでこれはまた今度

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