[Laravel] Eloquent リレーションと Eager Loading

「クエリが大量に発行されているせいでタイムアウトしちゃってる」
といったことを避けるために重要な"Eager loading"。

Eloquent リレーションで Eager loading するにはなんとなく `with` メソドを使うということぐらいしか知らなかったので、
ドキュメントの `Eloquent: Relationships > Eager loading` の部分を読んでみました。

### 参考
公式ドキュメント
https://laravel.com/docs/5.6/eloquent-relationships#eager-loading

## Eloquent のリレーション

EloquentのリレーションはEloquentモデルにメソドで定義をする。

```php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Get all of the posts for the user.
     */
    public function posts()
    {
        return $this->hasMany('App\Post');
    }
}
```

呼び出すときは

```php
$user->posts()->where('active', 1)->get();
```

という感じ。
リレーションメソドではクエリビルダが使える。

## リレーションメソド と 動的プロパティ

リレーションクエリに制約を追加する必要がない場合、プロパティのように `->` でリレーションにアクセスすることができ、リレーションメソドに対してこれは「動的プロパティ」という。

```php
$user = App\User::find(1);

//->posts() じゃなく、 ->posts になってるのが動的プロパティ
foreach ($user->posts as $post) {
    //
}
```

## リレーションデータの Eager Loading

- 全部のデータをとってきて、
- （動的）プロパティで毎回リレーションデータをとってくる。

というようにしてしまうと、レコード数分だけクエリが発行されてしまう。

```php
//lazy loading
$books = App\Book::all(); //クエリを1回発行したあと、

foreach ($books as $book) {
    echo $book->author->name; //booksの冊数分（N回）だけ発行される。
}
```
よく言う「N+1」問題というやつ。
クエリを1回+N回発行してしまうと、大規模なデータになるほど、データをとるのにものすごく時間がかかってしまってタイムアウトになってしまうこともある。

これを避けるため、先にリレーションデータも取得しておくのが "Eager loading" であり、Eloquentでは `with` メソドを使う。

```php
//Eager loading
$books = App\Book::with('author')->get(); //クエリが発行されるのは2回だけ

foreach ($books as $book) {
    echo $book->author->name;
}
```
こうやって `with` メソドを使えば、"Eager loading" でリレーションデータを取得することができ、クエリの発行はたった2回で済む。

### リレーションメソドを使ってしまうと Eager loading にしてても意味がないので注意

```php
$books = App\Book::with('author')->find(1);;
$author = $user->author()->where('id', 1)->get();
```

リレーションメソドの場合、先ほどの例のように `...->where('active', 1)->...` とクエリビルダを使って制約が追加することができる。
ただし、毎回クエリを発行してデータを取りに行ってしまうことになるため、先にEager Loadingでリレーションデータを取ってきていても、そのあとにリレーションメソドを使ってしまうと意味がない。

一方の動的プロパティはキャッシュが聞くため、EloquentでEager Loadingをする場合は動的プロパティの方を使う。

参考
https://qiita.com/katsunory/items/d4757b8d9ac099a5291e

## Eager Loading のいろんなやり方

### 複数のリレーションをとることもできる

`with` の引数に配列を使って、複数のリレーションデータをとることもできる。

```php
$books = App\Book::with(['author', 'publisher'])->get();
```

### ネストされたテーブルのリレーションだってとることができる

「リレーションされたテーブルのリレーション」というネストされたテーブルのデータもEager loadingで取得することができる。
これは知らんかった。今度から使おう。

```php
$books = App\Book::with('author.contacts')->get();
```

### 特定のカラムだけをとることもできる

```php
$users = App\Book::with('author:id,name')->get();
```

この場合、 `id` は必ずとる必要があることに注意。

### Eager loadingするリレーションデータを限定したいとき

「titleにfirstを含むリレーションデータだけをEager Loadingしたい」というときは `with` の中でクロージャを使えば可能。

```php
$users = App\User::with(['posts' => function ($query) {
    $query->where('title', 'like', '%first%');
}])->get();
```

#### 注意点

`posts` のクエリでデータがとれないユーザーは `$users` に入ってこないのかと思っていたが、その場合は `posts` がnullになるだけで `$users` には入ってくる。
（ `with` はリレーションデータをとってくるメソド）

「titleにfirstを含むリレーションデータを持たないユーザーはいらない」と、リレーションデータをとってきたいのではなく **リレーションデータによる絞り込み** をしたい場合は `whereHas` メソドを使う。

```php
$users = App\User::whereHas('posts', function($query) {
    $query->where('title', 'like', '%first%');
})->get();
```


### 「先に親データとっちゃったけど、追加で子のデータもEager Loadingしたい」というのもできる

「すでに親のデータだけとっちゃったけど、リレーションデータをEager loadingでとりたい」というときには `load` メソドを使う。
（ドキュメントでは"Lazy Eager loading"と表記されている）

```php
$books = App\Book::all();

if ($someCondition) {
    $books->load('author', 'publisher');
}
```

`load` メソドも `with` と同様、追加でクエリを発行することが可能。

```php
$books->load(['author' => function ($query) {
    $query->orderBy('published_date', 'asc');
}]);
```

##### Eager loadし忘れたやつだけとる、という `loadMissing ` メソドもある

```php
public function format(Book $book)
{
    $book->loadMissing('author');

    return [
        'name' => $book->name,
        'author' => $book->author->name
    ];
}
```

「`all` メソドで実装しちゃった...」という経験は何度かあるので、この `load` `loadMissing` メソドは覚えておくと便利そう。

## まとめ
- Eloquentのリレーションはリレーションメソドを使う
    - リレーションメソドではクエリビルダ、チェーンメソドが使える。
- 制約を追加する必要がないのであれば動的プロパティとしてアクセスすることができる
- EloquentでEager Loadingする場合は `with` メソドを使って、データは動的プロパティでとってくる
- EloquentにはEager Loadingする手段がいくつも用意されている
    - あとからEager Loadとかもできる