[Laravel] 動的プロパティと Eager Loading

Laravel の動的プロパティと Eager Loading について解説します。
下記の表を見て何のことかわからないという人向けです。

|        |       動的プロパティ |    リレーションメソッド    |
|:-----------------|------------------:|:------------------:|
| (例)            |  $user->posts |   $user->posts()      |
| Lazy Loading (遅延ロード)            |             する |        しない        |
| Eager Loading             |              可能 |        不可能        |
| クエリビルダとして動作          |            しない |       する       |
| 返すオブジェクト               |     Collection |  リレーション         |


## 動的プロパティとは

リレーションをプロパティのようにアクセスできる仕組みです。

（例） User に複数の Post が紐づく「1対多」のリレーションを考えます。

```php:User.php
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}
```

```php:Post.php
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Post extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}
```

↓ これが動的プロパティです。

```php
$user = User::find(1);
$user->posts;
```

動的プロパティは `Illuminate\Database\Eloquent\Collection` を返します。

また，動的プロパティは遅延ロードします。実際にアクセスした時のみデータが読み込まれます。そのため，後で解説する「N＋１問題」が発生してしまいます。

## リレーションメソッドとは

Eloquent リレーションはクエリビルダとして動作します。
動的プロパティと違い，Lazy Loading はしません。

↓ これがリレーションメソッドです。

```php
$user = User::find(1);
$user->posts();  // Illuminate\Database\Eloquent\Relations\HasMany を返す
$user->posts()->get();  // Illuminate\Database\Eloquent\Collection を返す
```

リレーションメソッドは `Illuminate\Database\Eloquent\Relations\HasMany` を返しています。
get メソッドで繋げると，動的プロパティと同様に Collection を返します。

Eloquent リレーションはクエリビルダとして動作するので，自由にクエリ制約を加えることができます。

```php
$user = User::find(1);
$user->posts()->where('is_published', 1)->get();
```

## N＋１問題とは

全 User を取得し，ループで各ユーザーに紐づく Posts にアクセスする状況を考えます。

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

foreach ($users as $user) {
    $user->posts;
}
```

ユーザーが10人の場合，11回のSQLクエリが発行されています。

```sql
SELECT * FROM users
SELECT * FROM posts WHERE posts.user_id = 1 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 2 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 3 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 4 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 5 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 6 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 7 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 8 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 9 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 10 AND posts.user_id IS NOT NULL
```

見ての通り，クエリが大量に発行されてしまいます。これが「N + 1問題」です。

## N＋１問題の解決策

### withメソッドを使う

```php
$users = User::with(['posts'])->get();  // Illuminate\Database\Eloquent\Collection を返す
```

`dd(User::with(['chats'])->get())` でコレクションの中身を見てみると
各々の User インスタンスの中に，`relations` という項目があります。
その中に posts コレクションが格納されています。

with を使わずに `dd(User::all())` とすると `relations` は空になります。

SQL を見ると，2回しか発行されていないことがわかります。
全 User に紐づく Posts を1つのクエリで取得しています。

```sql
SELECT * FROM users
SELECT * FROM posts WHERE posts.user_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9. 10)
```

### with は何を返すのか

`dd(User::with(['posts']))` をすると `Illuminate\Database\Eloquent\Builder` を返していることがわかります。with メソッドはクエリビルダとして動作しています。
つまり，クエリを追加することも可能です。

```php
// この where は User に対する制約であることに注意
User::with(['posts'])->where('name', 'Alice')->get();
```

## さまざまな Eager Loading

**複数の Eager Loading**

```php
$users = User::with(['posts', 'articles'])->get();
```

**ネストした Eager Loading**

```php
$users = User::with(['posts.comments'])->get();
```

**遅延 Eager Loading**

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

if ($someCondition) {
    $users->load('posts', 'articles');
}
```

**制約付き Eager Loading**

```php
// 公開済み（ is_published が1）の Posts に限定する
// 条件に一致する Posts を1つも持たない User も取得される点に注意
$users = User::with(['posts' => function ($query) {
    $query->where('is_published', 1);
}])->get();
```

## 動的プロパティの仕組み

```php
$user = User::find(1);
$user->posts;
```

`posts()` のように `()` が付いていないので，posts メソッドは呼ばれません。
かと言って posts プロパティが定義されている訳でもありません。
この場合，マジックメソッドの `__get` が呼ばれます。

`Illuminate\Database\Eloquent\Model` を見てみます。

```php:Illuminate\Database\Eloquent\Model.php
// 引数 $key にはアクセスしたプロパティ名（ここでは 'posts' ）がセットされます
public function __get($key)
{
    return $this->getAttribute($key);
}
```

[Illuminate\Database\Eloquent\Model::__get](https://github.com/illuminate/database/blob/da1d6bbb9e881955407116c763e5094a5adfb296/Eloquent/Model.php#L2021-L2030)

`__get()` はマジックメソッドです。存在しない又はアクセス不能なプロパティにアクセスされた際に呼び出されます。

`getAttribute()` は `Illuminate\Database\Eloquent\Model.php` には存在しません。
use している HasAttributes トレイトに実装されています。

```php:Illuminate\Database\Eloquent\Model.php
namespace Illuminate\Database\Eloquent;

abstract class Model 
{
    use Concerns\HasAttributes,
        Concerns\HasRelationships,
}
// ※その他は省略
```

[この部分で use しています](https://github.com/illuminate/database/blob/da1d6bbb9e881955407116c763e5094a5adfb296/Eloquent/Model.php#L28)

トレイトはコードを再利用するための仕組みです。機能をまとめたものです。

```php:Illuminate/Database/Eloquent/Concerns/HasAttributes.php
public function getAttribute($key)
{
    if (! $key) {
        return;
    }

    if (array_key_exists($key, $this->attributes) ||
        array_key_exists($key, $this->casts) ||
        $this->hasGetMutator($key) ||
        $this->hasAttributeGetMutator($key) ||
        $this->isClassCastable($key)) {
        return $this->getAttributeValue($key);
    }

    if (method_exists(self::class, $key)) {
        return;
    }

    return $this->getRelationValue($key);
}
```

[Illuminate/Database/Eloquent/Concerns/HasAttributes::getAttribute](https://github.com/illuminate/database/blob/da1d6bbb9e881955407116c763e5094a5adfb296/Eloquent/Concerns/HasAttributes.php#L403-L434)

最後に return してる `getRelationValue()` を見てみます。

```php:Illuminate/Database/Eloquent/Concerns/HasAttributes.php
public function getRelationValue($key)
{
    // 既にキー 'posts' が relations 配列に存在すればそれを返す
    // 2回目以降はキャッシュを返す
    if ($this->relationLoaded($key)) {
        return $this->relations[$key];
    }

    if (! $this->isRelation($key)) {
        return;
    }

    if ($this->preventsLazyLoading) {
        $this->handleLazyLoadingViolation($key);
    }

    return $this->getRelationshipFromMethod($key);
}
```

[Illuminate/Database/Eloquent/Concerns/HasAttributes::getRelationValue](https://github.com/illuminate/database/blob/da1d6bbb9e881955407116c763e5094a5adfb296/Eloquent/Concerns/HasAttributes.php#L458-L485)

`relationLoaded()` を見てみます。

```php:Illuminate/Database/Eloquent/Concerns/HasRelationships.php
protected $relations = [];

// 'posts' というキーが relations 配列に存在しているかを判定
public function relationLoaded($key)
{
    return array_key_exists($key, $this->relations);
}
```

[Illuminate/Database/Eloquent/Concerns/HasRelationships::relationLoaded](https://github.com/illuminate/database/blob/da1d6bbb9e881955407116c763e5094a5adfb296/Eloquent/Concerns/HasRelationships.php#L778-L787)

最後に return してる `getRelationshipFromMethod()` を追います。

```php:Illuminate/Database/Eloquent/Concerns/HasAttributes.php
// 引数 $method には 'posts' が渡される
protected function getRelationshipFromMethod($method)
{
    // $user->posts() の実行結果を $relation に格納
    $relation = $this->$method();

    if (! $relation instanceof Relation) {
        if (is_null($relation)) {
            throw new LogicException(sprintf(
                '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method
            ));
        }

        throw new LogicException(sprintf(
            '%s::%s must return a relationship instance.', static::class, $method
        ));
    }

    // $relation->getResults() の結果が $results にセットされる
    // クロージャの処理を実行した後，$relation->getResults() の結果が返される
    // $relation->getResults() でDBからデータを取得
    return tap($relation->getResults(), function ($results) use ($method) {
        // 動的プロパティに値をセット
        $this->setRelation($method, $results);
    });
}
```

[Illuminate/Database/Eloquent/Concerns/HasAttributes::getRelationshipFromMethod](https://github.com/illuminate/database/blob/da1d6bbb9e881955407116c763e5094a5adfb296/Eloquent/Concerns/HasAttributes.php#L514-L541)

`tap` の中にある `setRelation()` は， Model.php で use している HasRelationships トレイトにあります。

```php:Illuminate/Database/Eloquent/Concerns/HasRelationships.php
protected $relations = [];

public function setRelation($relation, $value)
{
    // relations['posts'] = 実際の Posts データ
    $this->relations[$relation] = $value;

    return $this;
}
```

[Illuminate/Database/Eloquent/Concerns/HasRelationships::setRelation](https://github.com/illuminate/database/blob/da1d6bbb9e881955407116c763e5094a5adfb296/Eloquent/Concerns/HasRelationships.php#L789-L801)

`$this->relations[$relation] = $value;` の部分で実際のデータをセットしています。


## さいごに

動的プロパティと Eager Loading について解説しました。
間違っている点などありましたら教えてください。