Deep Dive into Laravel Eloquent ORM

## はじめに
過去にLaravel（5.6）のリクエストライフサイクルを中心にご紹介しました。

[Deep Dive into Laravel](https://qiita.com/prograti/items/750a1d17021498885b6d)

今回はEloquentの基本動作とリレーションについてご紹介したいと思います。その他のコレクションやミューテタなどの機能はここでは触れませんので公式サイトをご覧ください。

[公式サイト]Eloquent
https://laravel.com/docs/5.6/eloquent

## 処理の流れ

Eloquentの仕組みを見るために、まずは簡単なCRUD操作を例に基本的な内部処理の流れを見てみたいと思います。

:one: **Read**

以下のようなモデルからデータを取得する処理の流れを順番に見てみます。

```php
public function show($id)
{
  $user = User::find($id);
}
```

処理の流れを簡略化したものが以下のようになります（実際はもう少し複雑ですが分かりやすくするために省略しています）。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/0fa374f7-39dc-f3f2-0ff9-c1248ea772d8.gif)

まず、アプリケーションモデルが継承する基底ModelがEloquentBuilderにfindの処理を依頼します。EloquentBuilderはModelとQueryBuilderを仲介するAdapterのような存在で、ORMの処理を組み立てる中心的な役割を果たします。なお、EloquentBuilderはマクロを使って独自に拡張することもできます。

```App\Providers\AppServiceProvider.php
use Illuminate\Database\Eloquent\Builder;

public function register()
{
  // 拡張例（ロックしてeagerロードする）
  Builder::macro('loadWithLock', function($relations){
    $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations);
    foreach ($eagerLoad as $name => $closure) {
      $eagerLoad[$name] = function($relation) use ($closure) {
        $closure($relation);
        $relation->lockForUpdate();
      };
    }
    $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);
    return $this;
  });
}
```

```php
// 使用例
$posts = Post::loadWithLock('comments')->lockForUpdate()->find(1);
```

QueryBuilderはfrom句、join句、where句、group句、having句、order句、limit句、offset句、union句などの情報を保持する器を持っており、EloquentBuilderはその器にデータをセットします。このケースだとfrom句に'users'、where句に'users.id = 1'、limit句に'1'となります。

from句にセットされるテーブル名について補足すると、テーブル名はPluralizerというクラスを使ってモデル名から複数形の単語を生成しています（実際の生成はDoctrineのInflectorというクラスが行います）。ただし、data（datumの複数形）のような単語はそのままテーブル名として使用されます。特殊な単語の一覧はPluralizerとInflectorでそれぞれ以下のように定義されています。

```Illuminate\Support\Pluralizer.php
public static $uncountable = [
  'audio','bison','cattle','chassis','compensation','coreopsis',
  'data','deer','education','emoji','equipment','evidence',
  'feedback','firmware','fish','furniture','gold','hardware',
  'information','jedi','kin','knowledge','love','metadata',
  'money','moose','news','nutrition','offspring','plankton',
  'pokemon','police','rain','rice','series','sheep',
  'software','species','swine','traffic','wheat',
];
```

```Doctrine\Common\Inflector\Inflector.php
private static $uninflected = array(
  '.*?media', 'Amoyese', 'audio', 'bison', 'Borghese', 'bream', 'breeches',
  'britches', 'buffalo', 'cantus', 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'compensation', 'Congoese',
  'contretemps', 'coreopsis', 'corps', 'data', 'debris', 'deer', 'diabetes', 'djinn', 'education', 'eland',
  'elk', 'emoji', 'equipment', 'evidence', 'Faroese', 'feedback', 'fish', 'flounder', 'Foochowese',
  'Furniture', 'furniture', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'gold', 
  'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', 'jackanapes', 'jedi',
  'Kiplingese', 'knowledge', 'Kongoese', 'love', 'Lucchese', 'Luggage', 'mackerel', 'Maltese', 'metadata',
  'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', 'nutrition', 'offspring',
  'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'plankton', 'pliers', 'pokemon', 'police', 'Portuguese',
  'proceedings', 'rabies', 'rain', 'rhinoceros', 'rice', 'salmon', 'Sarawakese', 'scissors', 'sea[- ]bass',
  'series', 'Shavese', 'shears', 'sheep', 'siemens', 'species', 'staff', 'swine', 'traffic',
  'trousers', 'trout', 'tuna', 'us', 'Vermontese', 'Wenchowese', 'wheat', 'whiting', 'wildebeest', 'Yengeese'
);
```

artisanコマンドを使ってモデルの作成およびマイグレーションを行っていればテーブル名を解決してくれますので特に意識する必要はありませんが、自分でテーブルを作る場合は上記単語は気を付ける必要があります。

話を元に戻して、QueryBuilderにwhere句やlimit句をセットした後、スコープが設定されていればスコープに定義されたwhere句などの情報を追加でセットします。

```php
class User extends Model
{
  protected static function boot()
  {
    parent::boot();

    static::addGlobalScope('prefecture', function (Builder $builder) {
      $builder->where('prefecture', '=', '13');
    });
  }
}
```

全ての情報がセットし終わるとQueryBuilderにクエリの実行を依頼します。QueryBuilderはGrammar（MySql、Postgres、SQLite、SQL Serverの4種類）を使ってwhere句やlimit句の情報をプリペアドステートメントにコンパイルします。そしてコンパイルされたプリペアドステートメントとバインドパラメータをConnectionに渡し、Connectionがクエリを実行します。実行結果はEloquentBuilderでモデルに変換され、`retrieved`イベントが発行されて処理が終了となります。

:bulb: **モデルイベント**

Eloquentではモデル操作の前後で様々なイベントを発行します。モデルの`dispatchesEvents`プロパティにアプリケーションで作成したイベントを登録し、イベントリスナーでキャッチすることで処理をフックすることができます（creating、updatingなどの～ing系はリスナーでfalseをリターンすれば処理を中断することができる）。

```php
namespace App;

class User extends Model
{
  protected $dispatchesEvents = ['retrieved' => \App\Events\UserRetrieved::class];
}
```

```php
namespace App\Events;

use App\User;

class UserRetrieved
{
  public $user;

  public function __construct(User $user)
  {
    $this->user = $user;
  }
}
```

```php
namespace App\Listeners;

use App\Events\UserRetrieved as UserRetrievedEvent;

class UserRetrieved
{
  public function handle(UserRetrievedEvent $event)
  {
    $user = $event->user;
    // do something
  }
}
```

```php
class EventServiceProvider extends ServiceProvider
{
  protected $listen = [
    \App\Events\UserRetrieved::class => [
      \App\Listeners\UserRetrieved::class,
    ],
  ];
}
```

また、モデルにはObserverを登録するための`observe`メソッドが用意されていますので、Observerを利用すれば複数のイベントの処理を管理することができます。

```php
namespace App\Observers;

use App\User;

class UserObserver
{
  public function creating(User $user)
  {
    // do something
  }
    
    
  public function updating(User $user)
  {
    // do something
  }
}
```

```php
class AppServiceProvider extends ServiceProvider
{
  public function boot()
  {
    User::observe(UserObserver::class);
  }
}
```

:notebook_with_decorative_cover: **Read系メソッド一覧**

Read系メソッドでよく使うものをリストアップしてご紹介します。（:fire:は発行されるイベント）

:black_small_square: **all**
:fire: retrieved

レコード全件を取得します。

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

:black_small_square: **find**
:fire: retrieved

主キーで検索し、該当するレコードを取得します。

```php
$user = User::find(1); // 主キーの値が1のレコードを取得
$users = User::find([1, 2]); // 主キーの値が1か2のレコードを取得
$user = User::find(1, ['id', 'name']); // 取得するカラムを指定
```

:black_small_square: **findOrFail**
:fire: retrieved

主キーで検索し、該当するレコードがない場合は`ModelNotFoundException`をスローします（例外をキャッチしない場合は例外ハンドラで`NotFoundHttpException`にチェーンされて404ページへ遷移する）。

```php
$user = User::findOrFail(1);
```

:black_small_square: **findOrNew**
:fire: retrieved

主キーで検索し、該当するレコードがない場合はnewしたモデルインスタンスを返します。

```php
$user = User::findOrNew(1); // 下と同じ
$user = User::find(1) ?? new User;
```

:black_small_square: **firstOrNew**
:fire: retrieved

attributeにマッチしたレコードの1レコード目を返します。該当するレコードがない場合はnewしたモデルインスタンスに指定したattributeをセットして返します。

```php
$user = User::firstOrNew(['name' => 'qiita']); // レコードがない場合はnewしたモデルにnameをセット
```

:black_small_square: **firstOrFail**
:fire: retrieved

条件に該当するレコードの1レコード目を返します。該当するレコードがない場合は`ModelNotFoundException`をスローします。

```php
$user = User::where('active', 1)->firstOrFail();
```

:black_small_square: **firstOr**
:fire: retrieved

条件に該当するレコードの1レコード目を返します。該当するレコードがない場合は指定したコールバック関数を実行した結果を返します。

```php
$user = User::where('active', 1)->firstOr(function(){
    return new User;
});
```

:black_small_square: **value**
:fire: retrieved

レコードセットの1レコード目から指定したシングルカラムの値を取得します。

```php
$age = User::where('age', '>', 20)->value('age');
```

:black_small_square: **get**
:fire: retrieved

クエリの結果を取得します。

```php
$users = User::where('age', '>', 20)
  ->orderBy('age', 'desc')
  ->get();
```

:black_small_square: **pluck**
:fire: retrieved

レコードセットから指定したカラムだけを取り出してコレクションとして返します。

```php
$ages = User::where('age', '>', 20)->pluck('age');
```

※retrievedイベントが発行されるのは指定したカラムのアクセサーや日付ミューテタ、属性キャストが設定されている場合のみです。

```php
class Post extends Model
{
  // アクセサー
  public function getTitleAttribute($value)
  {
    return strtoupper($value);
  }
}
```

:black_small_square: **cursor**
:fire: retrieved

レコードセットから一行ずつレコードを取得します。レコードセットが配列に全件分展開されないため、バッチ処理のような大量のデータを扱う場合にメモリの使用量を抑えることができます。

```php
foreach (User::where('active', 1)->cursor() as $user) {
  // do something
}
```

:black_small_square: **chunk**
:fire: retrieved

指定した件数ずつレコードを取得し、結果をコールバック関数に渡します。コールバック関数の中でfalseをリターンするとそこでレコードの取得は終了します。

```php
$bool = User::where('active', 1)->chunk(100, function($users, $page) {
  // do something
});
```

:black_small_square: **chunkById**
:fire: retrieved

指定した件数ずつレコードを取得し、結果をコールバック関数に渡します。chunkの方はOFFSETを使用しているため、後ろに行けば行くほどOFFSETの位置まで読み込むオーバーヘッドが大きくなりパフォーマンスが低下します。chunkByIdは`SELECT * FROM users WHERE id > ? ORDER BY id LIMIT 100;`のようにidを使って開始位置を移動するためOFFSETよりもパフォーマンスが良くなります。キーがオートインクリメントのIDで大量のレコードを扱う場合には、chunkよりもこちらを利用するとパフォーマンスの向上が期待できます。

```php
$bool = User::where('active', 1)->chunkById(100, function($users) {
  // do something
});
```

:black_small_square: **each**
:fire: retrieved

指定した件数ずつレコードを取得し、結果をコールバック関数に渡します。コールバック関数の中でfalseをリターンするとそこでレコードの取得は終了します。chunkとほぼ同じですが、chunkのコールバックの引数はレコードセットなのに対し、こちらは単一レコードになります。

```php
$bool = User::where('active', 1)->each(function($user) {
  // do something
}, 100);
```

:black_small_square: **paginate、simplePaginate**
:fire: retrieved

OFFSETを利用したページング検索結果を返します。paginateはページ番号のリンクを作成するためにレコードの件数をカウントするクエリを実行した上でOFFSETのクエリを実行しますが、simplePaginateは「次」「前」のリンクしか作成しないためOFFSETのクエリのみで済みます。

```php
// GETで送られてきたpage番号から10件を取得する
$users = User::where('active', 1)->paginate(10);
$users = User::where('active', 1)->simplePaginate(10);
```

:black_small_square: **fresh**
:fire: retrieved

モデルをDBから再取得し、新しいモデルインスタンスを返します。

```php
$user = User::find(1);
$user2 = $user->fresh(); // idが1のレコードを再取得する
var_dump($user === $user2); // false
```

:black_small_square: **refresh**
:fire: retrieved

モデルをDBから再取得し、取得したデータでモデルのattributeを更新します。

```php
$user = User::find(1);
$user2 = $user->refresh(); // idが1のレコードを再取得する
var_dump($user === $user2); // true
```


:two: **Create & Update**

以下のようなモデルの新規作成および更新する処理を流れを順番に見てみます。

```php
public function store(Request $request)
{
  $user= new User;
  $user->name = $request->name;
  $user->save();
}

public function update(Request $request, $id)
{
  $user = User::find($id);
  $user->name = $request->name;
  $user->save();
}
```

処理の流れを簡略化したものが以下のようになります。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/50d8c547-a017-3644-1f7c-9b3fb1ebf9d5.gif)

まず、基底モデルでsavingイベントが発行されます。そして、モデルがnewしたものか、既存のモデルかで処理が分岐します。

既存のモデルの場合、最初にupdatingイベントが発行されます。次にモデルのデータに変更があったかどうかをチェックします。変更がなかった場合はupdate処理はスキップされます。データに変更があった場合はモデルに更新日付がセットされます。次にEloquentBuilderがWHERE句のセットを行い、QueryBuilderがクエリの実行を行います。実行が終わるとupdatedイベントが発行され、更新情報に今回の更新データを同期させます。

※モデルは内部でDBからデータを取得した時点でのオリジナル情報、現在の情報、DBに反映された更新情報の3種類を保持しています。

```php
public function update(Request $request, $id)
{
  $user = User::find($id);

  var_dump($user->getOriginal()); // オリジナル情報

  $user->name = 'qiita';

  var_dump($user->getAttributes()); // 現在の情報
  var_dump($user->getChanges());  // DBに反映された更新情報
  var_dump($user->getDirty());  // 変更された情報

  $user->save();

  var_dump($user->getOriginal());
  var_dump($user->getAttributes());
  var_dump($user->getChanges());
}
```

```php
// getOriginal
array
  'id' => int 1
  'name' => string 'no name'
  'created_at' => string '2018-03-18 08:32:43'
  'updated_at' => null

// getAttributes - nameを変更したので情報も上書きされる
array
  'id' => int 1
  'name' => string 'qiita'
  'created_at' => string '2018-03-18 08:32:43'
  'updated_at' => null

// getChanges - まだDBに反映してないので空
array
  empty

// getDirty - nameを変更したので情報が追加される
array
  'name' => string 'qiita'

// getOriginal - DBに反映したのでオリジナル情報のnameも上書きされる
array
  'id' => int 1
  'name' => string 'qiita'
  'created_at' => string '2018-03-18 08:32:43'
  'updated_at' => string '2018-03-18 08:38:38'

// getAttributes - オリジナル情報と同じ
array
  'id' => int 1
  'name' => string 'qiita'
  'created_at' => string '2018-03-18 08:32:43'
  'updated_at' => string '2018-03-18 08:38:38'

// getChanges - クエリを実行してnameとupdated_atが更新されたので情報が追加される
array
  'name' => string 'qiita'
  'updated_at' => string '2018-03-18 08:38:38'
```

モデルをnewした場合、最初にcreatingイベントが発行されます。モデルに作成日付がセットされた後にQueryBuilderがinsertのクエリを実行します。もし、モデルの主キーがオートインクリメントのIDの場合、先程insertしたデータのIDを取得し、モデルにIDをセットします。クエリの実行が終わるとcreatedイベントが発行されます。

そして、後処理としてsavedイベントが発行され、もしモデルにtouchesプロパティが設定されている場合は親モデルの更新日付を更新します。

```php
class Profile extends Model
{
  // Profileに更新があった場合、親のUserの更新日付も更新する
  protected $touches = ['user'];

  public function user()
  {
    return $this->belongsTo('App\User');
  }
}
```

最後にDBからデータを取得した時点でのオリジナル情報を現在の情報で上書きします。

:notebook_with_decorative_cover: **Create & Update系メソッド一覧**

Create & Update系メソッドでよく使うものをリストアップしてご紹介します。

:black_small_square: **save**
:fire: saving、saved、creating、created、updating、updated

DBにレコードを保存します。新規モデルの場合はinsert、既存モデルの場合はupdateになります。

```php
// insert
$user = new User;
$user->name = 'qiita';
$success = $user->save();

// update
$user = User::find(1);
$user->active = 0;
$success = $user->save();

$profile = Profile::find(1);
$profile->bio = 'hello world';
$success = $profile->save(['touch' => false]); // 親モデルの更新日付を更新しない
```

:black_small_square: **create**
:fire: saving、saved、creating、created

指定したattributeでレコードを作成します（インスタンスを返しますがattributeはセットした項目とキー、作成日付、更新日付しか持っていません。DBからセレクトしている訳ではないので当然ではありますが一応念のため）。

```php
$post = new Post;
$post = $post->create([
  'user_id' => 1, 
  'title' => 'sample', 
  'content' => 'this is a sample page.'
]);
```

なお、createでattributeを渡すためにはモデルの`fillable`プロパティに許可するattributeを設定するか、`guarded`プロパティで禁止するプロパティから除外する必要があります。それ以外のattributeを渡した場合は例外がスローされます。

```php
class Post extends Model
{
  // user_id、title、contentを許可
  protected $fillable = ['user_id', 'title', 'content'];
}

class Post extends Model
{
  // user_idを禁止（デフォルトは全て）
  protected $guarded= ['user_id'];
}
```

:black_small_square: **forceCreate**
:fire: saving、saved、creating、created

モデルのfillabl/guardedプロパティの有無に関係なく、指定したattributeでレコードを作成します。

```php
$post = new Post;
$post = $post->forceCreate([
  'user_id' => 1, 
  'title' => 'sample', 
  'content' => 'this is a sample page.'
]);
```

:black_small_square: **insert**
:fire: なし

指定したattributeでレコードを作成します。QueryBuilderを直接呼び出しているため作成日付は自動的にセットされません。また、fillableプロパティやguardedプロパティも無効となります。

```php
$success = Post::insert([
  'user_id' => 1, 
  'title' => 'sample', 
  'content' => 'this is a sample page.',
  'created_at' => new \DateTime
]);

// bulk insert
$success = Post::insert([
  ['user_id' => 1, 
  'title' => 'sample', 
  'content' => 'this is a sample page.',
  'created_at' => new \DateTime],
  ['user_id' => 2, 
  'title' => 'sample', 
  'content' => 'this is a sample page.',
  'created_at' => new \DateTime],
]);
```

:black_small_square: **update**
:fire: saving、saved、updating、updated

変更するカラムをattributeで渡してレコードを更新します。

```php
$post = Post::find(1);
$success = $post->update(['title' => 'Hello World', 'content' => 'hello world.']);
```

※createと同じようにモデルにfillabl/guardedプロパティを設定しておく必要があります。もしくは以下のような方法で代替することもできます。

```php
$success = $post
  ->fillable(['title', 'content'])
  ->update(['title' => 'Hello World', 'content' => 'hello world.']);

$success = $post
  ->guard(['user_id'])
  ->update(['title' => 'Hello World', 'content' => 'hello world.']);
```

```php
Post::unguard(); // ガードを外す
$post = Post::find(1);
$success = $post->update(['title' => 'Hello World', 'content' => 'hello world.']);
Post::reguard(); // ガードを元に戻す
```

:black_small_square: **push**
:fire: saving、saved、updating、updated

モデルおよびリレーションのあるモデルもまとめてDBに保存します。

```php
$user = User::find(1);
$user->name = 'qiita';
$profile = $user->profile;
$profile->bio = 'hello world';
$success = $user->push(); // userに加えてprofileも更新される
```

:black_small_square: **firstOrCreate**
:fire: retrieved、saving、saved、updating、updated

attributeにマッチしたレコードの1レコード目を返します。該当するレコードがない場合はinsertを行います。

```php
// レコードがない場合はuser_id、title、contentの値を使ってinsertを行う
$post = Post::firstOrCreate(
  ['user_id' => 1, 'title' => 'sample'],
  ['content' => 'this is a sample page.']
);
```

※createと同じようにモデルにfillabl/guardedプロパティを設定しておく必要があります。

:black_small_square: **updateOrCreate**
:fire: retrieved、saving、saved、creating、created、updating、updated

attributeにマッチしたレコードを更新します。該当するレコードがない場合はinsertを行います。

```php
// レコードがない場合はuser_id、title、contentの値を使ってinsertを行う
$post = Post::updateOrCreate(
  ['user_id' => 1, 'title' => 'sample'],
  ['content' => 'hello world.']
);
```

※createと同じようにモデルにfillabl/guardedプロパティを設定しておく必要があります。

:black_small_square: **increment**
:fire: なし

指定したカラムの値を増やしてレコードを更新します。

```php
$post = Post::find(1);
$post->increment('page_views'); // +1
$post->increment('page_views', 10); // +10
```

:black_small_square: **decrement**
:fire: なし

指定したカラムの値を減らしてレコードを更新します。

```php
$post = Post::find(1);
$post->decrement('days_left'); // -1
$post->decrement('days_left', 10); // -10
```

:black_small_square: **replicate**
:fire: なし

モデルを複製して新しいインスタンスを作成します。複製されたモデルのattributeは元のモデルからキー、作成日付、更新日付を取り除いたものになります。

```php
$post = Post::find(1);
$clone = $post->replicate();

var_dump($post->exists); // 既存モデルのため true
var_dump($clone->exists); // 新規モデルのため false
var_dump($post->is($clone)); // 複製したモデルにはキーがないため false

$clone->save(); // 複製元と同じ情報で新しいレコードを作成

$clone = $post->replicate(['pageviews']); // 複製したくないattributeを指定することも可
```

:three: **Delete**

以下のようなモデルを削除する処理を流れを順番に見てみます。

```php
public function destroy($id)
{
  $user = User::find($id);
  $user->delete();
}
```

処理の流れを簡略化したものが以下のようになります。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/4c596570-28ef-04c6-ddfc-2c699c3a25e1.gif)

まず、基底モデルでdeletingイベントが発行されます。次にモデルにtouchesプロパティが設定されている場合は親モデルの更新日付を更新します。EloquentBuilderがWHERE句のセットを行いQueryBuilderがクエリの実行を行います。実行が終わるとdeletedイベントが発行されます。

:notebook_with_decorative_cover:  **Delete系メソッド一覧**

Delete系メソッドでよく使うものをリストアップしてご紹介します。

:black_small_square: **delete**
:fire: deleting、deleted

レコードを削除します（モデルのソフトデリートが有効な場合は論理削除）。

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

:black_small_square: **forceDelete**
:fire: deleting、deleted、forceDeleted

レコードを物理削除します。

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

:black_small_square: **destroy**
:fire: deleting、deleted

キーを指定してレコードを削除します。

```php
$count = User::destroy(1);
$count = User::destroy([1, 2, 3]);
```

:black_small_square: **truncate**
:fire: なし

テーブルをtruncateします。

```php
User::truncate();
```

:black_small_square: **restore**
:fire: restoring、restored、saving、saved、updating、updated

論理削除されたレコードを元に戻します。

```php
$users = User::onlyTrashed()->get();
foreach ($users as $user) {
  $user->restore();
}
```

## リレーション

基本的な処理の流れを見ましたので、次はリレーションの仕組みを見てみたいと思います。

:one: **1対1（hasOne、belongsTo）**

親モデルが子モデルと1対1のリレーションを持っている場合、親モデルでは`hasOne`メソッドを使って子モデルとのリレーションを定義します。

```php
class User extends Model
{
  public function profile()
  {
    return $this->hasOne('App\Profile');
  }
}
```

子モデルを取得する場合には以下のように親モデルのattributeとしてアクセスします。

```php
public function show($id)
{
  $user = User::find($id);
  $profile = $user->profile;
}
```

この時の処理の流れを簡略化したものが以下のようになります。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/7f778813-0831-ce52-5d52-ec010349da80.gif)

まず、モデルのattributeにアクセスすると基底クラスはモデル自身が持つattributeなのか、そうでないのかを判断し、そうでない場合はモデルに定義されたメソッドを実行します。

モデルはメソッドのリターンとしてhasOneメソッドを使ってHasOneオブジェクトを基底クラスに返します。hasOneメソッドは引数として子モデルのクラス名、外部キー、ローカルキーを指定します。

```php
return $this->hasOne('App\Profile', '外部キー', 'ローカルキー');
```

外部キーを指定しない場合、親モデルのクラス名のスネークケースとキーをアンダースコアでつなげた形になります（例：user_id）。

```Illuminate\Database\Eloquent\Model.php
public function getForeignKey()
{
  return Str::snake(class_basename($this)).'_'.$this->getKeyName();
}
```

ローカルキーとは子モデルの外部キーが参照する親モデルのキーを表します。指定しない場合は親モデルのキー（id）になります。実行されるクエリのイメージは以下のようになります。

```sql
SELECT * FROM 子モデル WHERE 子モデルの外部キー = 親モデルのキー（ローカルキー）の値
```

基底クラスは実行したクエリの結果をEloquentBuilderから受け取りモデルに返します。なお、取得した結果はロード済みリレーションとして保持され、同一リクエスト内であればattributeに何回アクセスしてもクエリが実行されるのは最初の1回だけになります。

```php
public function show($id)
{
  $user = User::find($id);
  $profile = $user->profile;  // クエリが実行される
  $profile = $user->profile;  // 保持している結果を返すのでクエリは実行されない

  $user = User::find($id);
  $profile = $user->profile;  // 最初の$userとはインスタンスが異なるのでクエリが実行される
}
```

なお、hasOneは`withDefault`メソッドを使ってデフォルトモデルを設定することができます。親モデルに紐づく子モデルがない場合、子モデルのインスタンスを生成して返します。子モデルのattributeの外部キーには親モデルのキー（ローカルキー）の値がセットされます。

```php
// データがない場合は new Profile(['user_id' => xxx]) が返される
return $this->hasOne('App\Profile')->withDefault();

// attributeを指定することも可能。fillableプロパティかguardedプロパティの設定が必要
return $this->hasOne('App\Profile')->withDefault(['bio' => 'hello world']);
```

次は子モデルを見てみましょう。子モデルでは`belongsTo`メソッドを使って親モデルとのリレーションを定義します。

```php
class Profile extends Model
{
  public function user()
  {
    return $this->belongsTo('App\User');
  }
}
```

親モデルを取得する場合には以下のように子モデルのattributeとしてアクセスします。

```php
public function show($id)
{
  $profile= Profile::find($id);
  $user= $profile->user;
}
```

この時の処理の流れを簡略化したものが以下のようになります。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/a556b4d5-98c6-4062-716c-f82e11627115.gif)

基本的に前述したhasOneと流れは同じです。モデルはメソッドのリターンとしてbelongsToメソッドを使ってBelongsToオブジェクトを基底クラスに返します。belongsToメソッドは引数として子モデルのクラス名、外部キー、オーナーキー、リレーション名を指定します。


```php
return $this->belongsTo('App\User', '外部キー', 'オーナーキー', 'リレーション名');
```

外部キーを指定しない場合、リレーション名のスネークケースと親モデルのキーをアンダースコアでつなげた形になります。リレーション名はデフォルトでbelongsToメソッドを呼び出したfunctionの名前が設定されます（userというメソッドで呼び出していたらuserになる）。

```Illuminate\Database\Eloquent\Concerns\HasRelationships.php
protected function guessBelongsToRelation()
{
  list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);

  return $caller['function'];
}
```

オーナーキーとは親モデルのキーを表します。指定しない場合は親モデルのキー（id）になります。実行されるクエリのイメージは以下のようになります。

```sql
SELECT * FROM 親モデル WHERE オーナーキー（親モデルのキー） = 子モデルの外部キーの値
```

なお、belongsToは`withDefault`メソッドを使ってデフォルトモデルを設定することができます。子モデルに紐づく親モデルがない場合、親モデルの空のインスタンスを生成して返します。

```php
// データがない場合は new User() が返される
return $this->belongsTo('App\User')->withDefault();

// attributeを指定することも可能。fillableプロパティかguardedプロパティの設定が必要
return $this->belongsTo('App\User')->withDefault(['name' => 'anonymous']);
```

:bulb: **リレーションモデルのinsert、updateおよびリレーションの更新**

子モデルのレコードを新規追加する場合、saveメソッドを使うとちょっと便利です（外部キーにも値が自動的に設定される）。

```php
$user = User::find(1);
$profile = new Profile(['nick_name' => 'qiita']);

// 外部キー（user_id）に親モデルのキーが自動的に入る
$profile = $user->profile()->save($profile);
```

saveメソッド以外にも以下のようなメソッドが利用できます。

```php
// insert
$profile = $user->profile()->create(['nick_name' => 'qiita']);

// selectしてレコードがなければinsert
$profile = $user->profile()->firstOrCreate([], ['nick_name' => 'qiita']); 

// selectしてレコードがあればupdate、なければinsert
$profile = $user->profile()->updateOrCreate([], ['nick_name' => 'qiita']);
```

なお、上記メソッドで子モデルを追加しても親モデルのリレーションに追加される訳ではありません。

```php
$profile = $user->profile()->save($profile);

var_dump($user->getRelations()); // empty

$profile = $user->profile;

var_dump($user->getRelations()); // ['profile' => object(App\Profile)]
```

`associate`メソッドおよび`dissociate`メソッドを使ってbelongsToリレーションを更新することができます。

```php
class User extends Model
{
  public function organization()
  {
    return $this->belongsTo('App\Organization');
  }
}
```

```php
$organization = Organization::find(1);
$user = User::find(1);

// ユーザーのorganization_idに値がセットされ、リレーションが追加される
$user->organization()->associate($organization);
$user->save();

var_dump($user->getRelations()); // ['organization' => object(App\Organization)]

// ユーザーのorganization_idにnullがセットされ、リレーションが解除される
$user->organization()->dissociate();

var_dump($user->getRelations()); // empty
```

:two: **1対多（hasMany、belongsTo）**

1対多は基本的に1対1と同じでほとんど違いはありません。hasOneの場合は返ってくる結果が1件ですが、hasManyの場合はコレクションが返ってきます。hasManyにはデフォルトモデルを設定することはできません。

リレーションはQueryBuilderとして動作しますので、条件を追加して絞り込むことができます（1対多に限らず他のリレーションも同様です）。

```php
$post = Post::find(1);
$comments = $post->comments()->where('user_id', 2)->get();
```

以下のようにリレーションモデルのレコードに基づいてモデルのレコードを絞り込むこともできます。

```php
// コメントのある投稿を取得
$post = Post::has('comments')->get();

// コメントか いいね がある投稿を取得
$post = Post::has('comments')->orHas('likes')->get();

// コメントがない投稿を取得
$post = Post::doesntHave('comments')->get();

// コメントがないか いいね がない投稿を取得
$post = Post::doesntHave('comments')->orDoesntHave('likes')->get();

// 直近10日間でコメントがあった投稿を取得
$post = Post::whereHas('comments', function($query) {
  $query->where('created_at', '>=', Carbon::now()->subDay(10));
})->get();

// 直近10日間でコメントか いいね　があった投稿を取得
$post = Post::whereHas('comments', function($query) {
  $query->where('created_at', '>=', Carbon::now()->subDay(10));
})->orWhereHas('likes', function($query) {
  $query->where('created_at', '>=', Carbon::now()->subDay(10));
})->get();

// 直近10日間でコメントがなかった投稿を取得
$post = Post::whereDoesntHave('comments', function($query) {
  $query->where('created_at', '>=', Carbon::now()->subDay(10));
})->get();

// 直近10日間でコメントか いいね　がなかった投稿を取得
$post = Post::whereDoesntHave('comments', function($query) {
  $query->where('created_at', '>=', Carbon::now()->subDay(10));
})->orWhereDoesntHave('likes', function($query) {
  $query->where('created_at', '>=', Carbon::now()->subDay(10));
})->get();
```

また、前述した1対1のsaveメソッドやcreateメソッド以外に、複数モデルのinsertに対応した`saveMany`メソッドや`createMany`メソッドが利用できます。

```php
$post = Post::find(1);
$posts = $post->comments()->saveMany([
  new Comment(['user_id' => 1, 'comment' => 'hoge']),
  new Comment(['user_id' => 2, 'comment' => 'hoge']),
]);

$posts = $post->comments()->createMany([
  ['user_id' => 1, 'comment' => 'hoge'],
  ['user_id' => 2, 'comment' => 'hoge'],
]);
```

firstOrCreateやupdateOrCreateを使って条件を指定したinsert、updateを行うこともできます。

```php
// user_idが2のコメントがなければuser_idが2のコメントをinsert
$post = $post->comments()->firstOrCreate(
  ['user_id' => 2], ['comment' => 'hoge']
);

// user_idが2のコメントがあればupdate、なければinsert
// ※ただし、user_idが2のコメントが複数あっても最初の1レコード目しか更新されない
$post = $post->comments()->updateOrCreate(
  ['user_id' => 2], ['comment' => 'hoge']
);
```

:three: **多対多（belongsToMany）**

モデルが多対多のリレーションを持っている場合、お互いのモデルは`belongsToMany`メソッドを使ってリレーションを定義します。

```php
class Post extends Model
{   
  public function tags()
  {
    return $this->belongsToMany('App\Tag');
  }
}
```

```php
class Tag extends Model
{   
  public function posts()
  {
    return $this->belongsToMany('App\Post');
  }
}
```

モデルを取得する場合には以下のようにモデルのattributeとしてアクセスします。

```php
public function show($id)
{
  $post = Post::find($id);
  foreach ($post->tags as $tag) {
    // do something
  }
}
```

この時の処理の流れを簡略化したものが以下のようになります。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/7422e4dc-e8b6-ac41-242b-94af71161feb.gif)

前述したhasOneと流れは大体同じです。モデルはメソッドのリターンとしてbelongsToManyメソッドを使ってBelongsToManyオブジェクトを基底クラスに返します。belongsToManyメソッドは引数としてリレーションモデルのクラス名、中間テーブル名、外部pivotキー、関連pivotキー、親キー、関連キー、リレーション名を指定します。

```php
return $this->belongsToMany('App\Tag', '中間テーブル名', '外部pivotキー', '関連pivotキー', '親キー', '関連キー', 'リレーション名');
```

中間テーブルとはリレーション元テーブルとリレーション先テーブルを紐づけるためのテーブルになります。例えば投稿に複数のタグが付けられるようなデータモデルの場合、投稿に付けられたタグを取得したり、タグが付いた投稿を取得するには、投稿とタグを紐づけるための中間テーブル（投稿IDとタグIDをもつようなテーブル）が必要になります。

中間テーブル名はデフォルトで互いのモデル名のスネークケースをアンダースコアで繋いだものになります（モデル名はsort関数でソート）。つまり、PostとTagというモデルがあった場合、'post_tag'が中間テーブル名になります。

```Illuminate\Database\Eloquent\Concerns\HasRelationships.php
public function joiningTable($related)
{
  $models = [
    Str::snake(class_basename($related)),
    Str::snake(class_basename($this)),
  ];
  sort($models);
  
  return strtolower(implode('_', $models));
}
```

中間テーブルとの結合条件には関連キーと関連pivotキーが使用されます。関連キーを指定しない場合はリレーション先テーブルのキー（id）になります。関連pivotキーを指定しない場合はリレーション先テーブルの外部キーとなり、モデル名のスネークケースとキーをアンダースコアでつなげた形になります。

```sql
INNER JOIN 中間テーブル ON リレーション先テーブル.関連キー = 中間テーブル.関連pivotキー
```

WHERE句には外部pivotキーと親キーが使用されます。外部pivotキーを指定しない場合はリレーション元テーブルの外部キーとなり、モデル名のスネークケースとキーをアンダースコアでつなげた形になります。親キーを指定しない場合はリレーション元テーブルのキー（id）が使用されます。

```sql
WHERE 中間テーブル.外部pivotキー = 親キーの値
```

また、SELECTするカラムにはリレーション先のカラムの他に中間テーブルのpivotキーが追加されます。

```sql
SELECT リレーション先テーブル.*, 中間テーブル.外部pivotキー, 中間テーブル.関連pivotキー
```

従って実際に実行されるクエリのイメージは以下のようになります。

```sql
SELECT tags.*, post_tag.post_id, post_tag.tag_id
FROM posts 
INNER JOIN post_tag -- 中間テーブル名
ON tags.id = post_tag.tag_id -- 関連キー、関連pivotキー
WHERE post_tag.post_id = ? -- 外部pivotキー、親キーの値
```

取得した中間テーブルのデータはPivotモデルに格納され`pivot`というキー名でロード済みリレーションに保持されますので、`pivot`という名前のattributeにアクセスすればデータを取得することができます。

```php
public function show($id)
{
  $post = Post::find($id);
  foreach ($post->tags as $tag) {
    $post_id = $tag->pivot->post_id;
  }
}
```

リレーションの定義で`as`メソッドを使えばpivotという名前を変更することができます。

```php
class Post extends Model
{   
  public function tags()
  {
    return $this->belongsToMany('App\Tag')->as('taginfo');
  }
}
```

また、リレーションの定義で`using`メソッドを使えば中間テーブルデータのモデルをPivotから独自のモデルへ変更することができます。

```php
class Post extends Model
{   
  public function tags()
  {
    return $this->belongsToMany('App\Tag')->using('App\TagInfo');
  }
}
```

```php
use Illuminate\Database\Eloquent\Relations\Pivot;

// Pivotモデルを継承する必要がある
class TagInfo extends Pivot
{
    
}
```

前述したようにデフォルトでは中間テーブルの外部キー2つしかデータを取得しないためそれ以外のカラムにはアクセスできません。しかし、リレーション定義時に`withPivot`や`withTimestamps`を使うことでカラムを追加することができます。

```php
class Post extends Model
{   
  public function tags()
  {
    return $this->belongsToMany('App\Tag', 'tag_infos') // 中間テーブル名をtag_infosに変更
      ->using('App\TagInfo') // モデルをPivotからTagInfoに変更
      ->as('taginfo') // taginfoというattributeでアクセスできるように変更
      ->wherePivot('genre', 1) // genreが1のデータに絞り込む
      ->withPivot('tag_order', 'tag_color') // 取得するカラムを追加
      ->withTimestamps(); // created_atとupdated_atを追加
  }
}
```

:bulb: **中間テーブルのデータ操作**

Eloquentには中間テーブルにデータを追加、削除するための便利なメソッドが用意されています。

```php
$post = Post::find(1);

// 中間テーブルに投稿IDが1、タグIDが1のデータを追加する
$post->tags()->attach(1);

// 中間テーブルにタグIDが1のデータを指定したattributeで追加する
$post->tags()->attach(1, ['tag_order' => 1, 'tag_color' => 'red']);

// 中間テーブルにタグIDが1および2のデータを指定したattributeで追加する
$post->tags()->attach([
  1 => ['tag_order' => 1, 'tag_color' => 'red'],
  2 => ['tag_order' => 2, 'tag_color' => 'blue']
]);

// 中間テーブルからタグIDが1のデータを削除する
$count = $post->tags()->detach(1);

// 中間テーブルからタグIDが1および2のデータを削除する
$count = $post->tags()->detach([1, 2]);

// 中間テーブルから投稿IDが1のデータを全て削除する
$count = $post->tags()->detach();

// 中間テーブルにタグIDが1および2のデータが無ければ追加し、それ以外のものがあれば削除する
$changes = $post->tags()->sync([1, 2]);

// 中間テーブルにタグIDが1および2のデータが無ければ追加・更新し、それ以外のものがあれば削除する
$changes = $post->tags()->sync([
  1 => ['tag_order' => 1, 'tag_color' => 'red'],
  2 => ['tag_order' => 2, 'tag_color' => 'blue']
]);

// 中間テーブルにタグIDが1および2のデータが無ければ追加・更新し、それ以外のものは残したまま
$changes = $post->tags()->syncWithoutDetaching([
  1 => ['tag_order' => 1, 'tag_color' => 'red'],
  2 => ['tag_order' => 2, 'tag_color' => 'blue']
]);

// 中間テーブルに指定されたタグIDのものがあれば削除、なければ追加する
$changes = $post->tags()->toggle([1, 2]);

// 中間テーブルに指定されたタグIDのものがあれば削除、なければ指定したattributeで追加する
$changes = $post->tags()->toggle([
  1 => ['tag_order' => 1, 'tag_color' => 'red'],
  2 => ['tag_order' => 2, 'tag_color' => 'blue']
]);

// 中間テーブルにタグIDが1のデータを追加
$tag = Tag::find(1);
$tag = $post->tags()->save($tag, ['tag_order' => 1, 'tag_color' => 'red']);

// 中間テーブルのタグIDが1のデータを更新
$count = $post->tags()->updateExistingPivot(1, ['tag_order' => 1, 'tag_color' => 'red']);
```

:four: **経由テーブル越しの多対多（hasManyThrough）**

例えば以下のようなデータモデルで、組織の投稿を取得するためには組織に所属するユーザーを経由して投稿を取得する必要があります。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/5da07761-69a4-be98-2409-331f9405bddf.gif)

このようなデータモデルに対応する場合、`hasManyThrough`メソッドを使ってリレーションを定義します。

```php
class Organization extends Model
{   
  public function posts()
  {
    return $this->hasManyThrough('App\Post', 'App\User');
  }
}
```

モデルを取得する場合には以下のようにモデルのattributeとしてアクセスします。

```php
public function show($id)
{
  $organization = Organization::find($id);
  foreach ($organization->posts as $post) {
    $title = $post->title;
  }
}
```

この時の処理の流れを簡略化したものが以下のようになります。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/72289221-b9dd-01a4-b127-e132675c5539.gif)

前述したbelongsToManyと流れは大体同じです。モデルはメソッドのリターンとしてhasManyThroughメソッドを使ってHasManyThroughオブジェクトを基底クラスに返します。hasManyThroughメソッドは引数としてリレーションモデルのクラス名、経由モデルのクラス名、ファーストキー、セカンドキー、ローカルキー、セカンドローカルキーを指定します。

```php
return $this->hasManyThrough('App\Post', 'App\User', 'ファーストキー', 'セカンドキー', 'ローカルキー', 'セカンドローカルキー');
```

第2引数で指定した経由テーブルが前述した多対多の中間テーブルと同等の役割を果たします。経由テーブルとの結合条件にはセカンドローカルキーとセカンドキーが使用されます。セカンドローカルキーを指定しない場合は経由テーブルのキー（id）になります。セカンドキーを指定しない場合は経由テーブルの外部キーとなり、モデル名のスネークケースとキーをアンダースコアでつなげた形になります。

```sql
INNER JOIN 経由テーブル ON 経由テーブル.セカンドローカルキー = 経由先テーブル.セカンドキー
```

WHERE句にはファーストキーとローカルキーが使用されます。ファーストキーを指定しない場合は経由元テーブルの外部キーとなり、モデル名のスネークケースとキーをアンダースコアでつなげた形になります。ローカルキーを指定しない場合は経由元テーブルのキー（id）になります。

```sql
WHERE 経由テーブル.ファーストキー = ローカルキーの値
```

また、SELECTするカラムには経由先テーブルのカラムの他にファーストキーが追加されます。

```sql
SELECT 経由先テーブル.*, 経由元テーブル.ファーストキー
```

従って実際に実行されるクエリのイメージは以下のようになります。

```sql
SELECT posts.*, users.organization_id 
FROM posts 
INNER JOIN users -- 経由元テーブル
ON users.id = posts.user_id -- セカンドローカルキー、セカンドキー
WHERE users.organization_id = ? -- ファーストキー、ローカルキーの値
```

:five: **ポリモーフィック リレーション（morphTo、morphMany）**

ポリモーフィック リレーションとは、1つのキーが持つリレーションが複数のモデルにまたがるようなものを表します。例えば以下のように投稿やコメントに対する「いいね」を一つのテーブルで管理する場合が該当します（target_typeとtarget_idで投稿に対するものかコメントに対するものかを管理する）。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/71d08214-63e0-63b3-5db9-6ed2fdd67bee.gif)

このようなデータモデルに対応する場合、`morphTo`メソッドと`morphMany`メソッドを使ってリレーションを定義します。

```php
class Post extends Model
{
  public function comments()
  {
    return $this->hasMany('App\Comment');
  }
 
  public function likes()
  {
    return $this->morphMany('App\Like', 'target');
  }
}
```

```php
class Comment extends Model
{
  public function likes()
  {
    return $this->morphMany('App\Like', 'target');
  }
}
```

```php
class Like extends Model
{   
  public function target()
  {
    return $this->morphTo();
  }
}
```

モデルを取得する場合には以下のようにモデルのattributeとしてアクセスします。

```php
public function show($id)
{
  $post = Post::find($id);
  foreach ($post->likes as $like) {
    $user_id = $like->user_id;
  }

  foreach ($post->comments as $comment) {
    foreach ($comment->likes as $like) {
      $user_id = $like->user_id;
    }
  }
}
```

この時の処理の流れを簡略化したものが以下のようになります。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/f1822946-053c-3b1a-2c11-fb2549dc42c5.gif)

基本的には1対1や1対多の流れと同じです。モデルはメソッドのリターンとしてmorphManyメソッドを使ってMorphManyオブジェクトを基底クラスに返します。morphManyメソッドは引数としてリレーションモデルのクラス名、morph名、morphタイプ、morph ID、ローカルキーを指定します。

```php
return $this->morphMany('App\Like', 'morph名', 'morphタイプ', 'morph ID', 'ローカルキー');
```

morphタイプを指定しない場合はmorph名と'type'をアンダースコアで繋げた文字列（xxxx_type）となり、morph IDを指定しない場合はmorph IDと'id'をアンダースコアで繋げた文字列（xxxx_id）となります。ローカルキーを指定しない場合はリレーション元テーブルのキー（id）になります。

```php
public function show($id)
{
  $post = Post::find($id);
  foreach ($post->likes as $like) {
    $user_id = $like->user_id;
  }
}
```

上記のように投稿の「いいね」を取得する場合に実行されるクエリのイメージは以下のようになります。

```sql
SELECT * FROM likes WHERE target_type = 'App\Post' AND target_id = （投稿idの値）
```

morphタイプの値にはモデルのクラス名が入ります。ただし、サービスプロバイダなどでmorphMapを設定することでクラス名から代替文字列に変更することができます。

```php
use Illuminate\Database\Eloquent\Relations\Relation;

public function boot()
{
  Relation::morphMap([
    'post' => 'App\Post',
    'comment' => 'App\Comment',
  ]);
}
```

ポリモーフィック リレーションを利用してさらに複雑なケースに対応することができます。例えば以下のデータモデルのように、中間テーブルを介して投稿やコメントに「いいね」したユーザーを取得したり、ユーザーが「いいね」した投稿やコメントを取得する多対多の場合です。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/2f00addf-c378-c182-8ec8-27cd0d89e5fd.gif)

この場合のリレーションの定義は`morphedByMany`メソッドと`morphToMany`メソッドを使用します。

```php
class User extends Model
{
  public function myLikedPosts()
  {
    return $this->morphedByMany('App\Post', 'target', 'likes');
  }

  public function myLikedComments()
  {
    return $this->morphedByMany('App\Comment', 'target', 'likes');
  }
}
```

```php
class Post extends Model
{
  public function likedUsers()
  {
    return $this->morphToMany('App\User', 'target', 'likes');
  }
}
```

```php
class Comment extends Model
{
  public function likedUsers()
  {
    return $this->morphToMany('App\User', 'target', 'likes');
  }
}
```

上記の例では第3引数にテーブル名を指定していますが、省略した場合は第2引数の文字列からテーブル名が決定されます（この場合はtargets）。

モデルを取得する場合には以下のようにモデルのattributeとしてアクセスします。

```php
public function show($id)
{
  $user= User::find($id);
  foreach ($user->myLikedPosts as $post) {
    $title = $post->title;
  }
}
```

また、前述した多対多の時と同様に中間テーブルのデータにもアクセスすることができます。

```php
class User extends Model
{
  public function myLikedPosts()
  {
    return $this->morphedByMany('App\Post', 'target', 'likes')
      ->as('like')
      ->withTimestamps();
  }
}
```

```php
public function show($id)
{
  $user= User::find($id);
  foreach ($user->myLikedPosts as $post) {
    $likedAt = $post->like->created_at;
  }
}
```

:six: **Eagerロード**

Eloquentではリレーションにアクセスする時、標準では遅延ロードになります。

```php
$posts = Post::all();
foreach ($posts as $post) {
  // このタイミングで投稿に紐づくコメントを取得するためのクエリが実行される
  $comments = $post->comments;
}
```

上記のような場合だとループの回数分クエリが実行され無駄にリソースを使ってしまう可能性があります。これを回避する方法としてEagerロードという仕組みがあり、Eagerロードを使うと先程のループのクエリが1回で済むようになります。Eagerロードを使うには以下のように`with`メソッドでリレーションを指定します。

```php
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
  $comments = $post->comments;
}
```

この時の処理の流れを簡略化したものが以下のようになります（例として1対多のリレーションがあった場合を取り上げています）。

![tmp.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/44845b3e-2db2-a586-d536-6d1faf9ba68e.gif)

まず、最初にwithメソッドで指定されたリレーションをパースします。withメソッドでは単一でリレーションを指定するだけでなく、様々な指定方法があります。

```php
// 単一のリレーションを指定
$posts = Post::with('comments')->get();

// 複数のリレーションを指定
$posts = Post::with(['comments', 'likes'])->get();

// コメントとそれに紐づくユーザーを指定
$posts = Post::with('comments.user')->get();

// コメントの特定のカラムだけ取得（PostとCommentはpost_idで紐づいているためpost_idは必須）
$posts = Post::with('comments:post_id,comment')->get();
```

パースしてリレーション情報を取り出すと、まずは大元のモデル（この場合Post）のfindを行います。この処理の流れは前述したものと同じなので説明は割愛します。findした結果を受け取るとリレーション情報からリレーションオブジェクト（この場合HasMany）を取得し、IN句に大元のモデルのキー（id）をセットします。もし、withメソッドで制約を定義していたら、そちらも追加されます。

```php
$posts = Post::with(['comments' => function($relation) {
  $relation->where('created_at', '>', Carbon::now()->subDay(3));
}])->get();
```

そしてクエリを実行して結果を取得するとロードされたリレーションとして大元のモデルにセットされます。この操作がループかつネストしていれば再帰的に行われモデルのツリーが作成されます。これにより、モデルのattributeにアクセスしてもクエリが実行されることなく、既に作成されたモデルが返されることになります。

もし、動的にリレーションを定義したい場合は`load`メソッドを使用してください。

```php
// コレクションとモデルのどちらでもloadメソッドは使用可
$posts = Post::all();
if (Auth::check()) {
  $posts->load('comments');
}

$post = Post::find(1);
if (Auth::check()) {
  $post->load('comments');
}
```

常にEagerロードを適用したい場合があるかもしれません。その場合は、モデルに`with`プロパティを設定することができます。

```php
class Post extends Model
{
  protected $with = ['comments'];
}
```

もし、withプロパティを設定していてEagerロードを適用したくないケースがあった場合は`without`メソッドを使用することでEagerロードを解除することができます。

```php
$posts = Post::without('comments')->get();
```

## 最後に

一通りEloquentの基本的な操作を処理の流れを追いながら紹介してみました。insertの部分で少し触れましたがEloquentを使っているつもりが実はQueryBuilderの方を使っていて意図した動作にならないということも起こりえますので十分にご注意ください。

また、Eloquentに限ったことではありませんがORMは非常に便利な反面、その特性や制限を理解していないとシステムが大きく複雑になるにつれてそのアドバンテージを消失してしまいます。EloquentはActiveRecordパターンを実装したORMですが、そのシンプルな設計思想が故に複雑なDB設計とはあまり相性が良くありません。そのような事も理解した上で設計が必要になるということも念頭に置いておいた方が良いかもしれません（システムは生き物なのでどんなに頑張って設計してもインピーダンスミスマッチが起こることは避けられませんが:sweat_smile:）。