Eloquentのリレーション活用方法【関連の取得】

Eloquentのリレーションメソッドを活用して、紐付くモデルを取得する方法について解説します。
リレーションメソッドを活用することで、少ない処理で目的のデータを取得できるようになります。

実際に実行されるSQLを載せています。どういった動作が行われるのか理解するのに役立つと思います。

なお、Eloquentのリレーションについて以下3回に渡って解説しています。他の記事も参考にしてください。

動作確認に利用したリポジトリです。
https://github.com/raku-raku/laravel_eloquent_practice

「関連モデル」を取得

動的プロパティで取得

動的プロパティで「関連モデル」を取得できます。

動的プロパティは __getマジックメソッド で実装されています。 Illuminate\Database\Eloquent\Model__getマジックメソッド 経由で各リレーションクラスの getResultsメソッド が呼ばれます。

>>> User::find(1)->phone
=> App\Models\Phone {#2939
     id: 1,
     user_id: 1,
     tel: "072-038-7531",
     created_at: "2018-11-15 06:38:14",
     updated_at: "2018-11-15 06:38:14",
   }

リレーションメソッドで取得

関連モデルを絞り込んで取得したい場合、メソッドとしてアクセスします。
メソッドとしてアクセスすると、条件絞り込みのメソッドを繋げることができます。

>>> User::find(1)->posts()->where('title', 'LIKE', '%この%')->toSql()
=> "select * 
    from `posts` 
    where `posts`.`user_id` = ? 
    and `posts`.`user_id` is not null 
    and `title` LIKE ?"
>>>
>>> User::find(1)->posts()->where('title', 'LIKE', '%この%')->get();
=> Illuminate\Database\Eloquent\Collection {#2963
     all: [
       App\Models\Post {#2926
         id: 6,
         user_id: 1,
         title: "手の方で、このそこですからだ。",
         body: "やの中はもう一つの方たいくほんとうの鼠ねずみいろの霧きりして誰だれから一生けん命めいめんの柱はしの前に女たちの流ながら答えましく立派りっぱいにあかり天の川の中でのでしたりしているだろう。こいつも窓まどかってるんです。その大きな橋はしばらく、船に乗",
         created_at: "2018-11-15 06:38:14",
         updated_at: "2018-11-15 06:38:14",
       },
     ],
   }

中間テーブルのカラム取得

中間テーブルのカラムは pivot 経由でアクセスできます。

>>> User::find(1)->roles()->first()
=> App\Models\Role {#2928
     id: 1,
     role: "管理者",
     created_at: "2018-11-15 06:38:14",
     updated_at: "2018-11-15 06:38:14",
     pivot: Illuminate\Database\Eloquent\Relations\Pivot {#2976
       user_id: 1,
       role_id: 1,
       column1: "King. 'It began with the Gryphon. 'Then, you.",
       column2: "So she swallowed one of the trees upon her knee.",
       created_at: "2018-11-15 06:38:14",
       updated_at: "2018-11-15 06:38:14",
     },
   }
>>> 
>>> User::find(1)->roles()->first()->pivot->column1
=> "King. 'It began with the Gryphon. 'Then, you."

「自モデル」を取得

関連が存在する自モデル

hasメソッド を利用すると、「関連モデル」が存在する「自モデル」を取得できます。
doesntHaveメソッド を利用すると、「関連モデル」が存在しない「自モデル」を取得できます。

>>> User::has('posts')->toSql()
=> "select * 
    from `users` 
    where exists (select * 
                  from `posts` 
                  where `users`.`id` = `posts`.`user_id`)"
>>> 
>>> User::doesntHave('posts')->toSql()
=> "select * 
    from `users` 
    where not exists (select * 
                      from `posts` 
                      where `users`.`id` = `posts`.`user_id`)"
>>> 
>>> User::has('posts')->pluck('name')
=> Illuminate\Support\Collection {#2967
     all: [
       "山岸 くみ子",
       "山本 直人",
       "山口 里佳",
     ],
   }

関連が特定条件で存在する自モデル

追加条件をつけたい場合、 whereHasメソッド whereDoesntHaveメソッド を利用します。

>>> Post::whereHas('comments', function ($query) {
...     $query->where('content', 'like', 'foo%');
... })->toSql()
=> "select * 
    from `posts` 
    where exists (select * 
                  from `comments` 
                  where `posts`.`id` = `comments`.`commentable_id` 
                  and `comments`.`commentable_type` = ? 
                  and `content` like ?)"

「関連モデル」の総数を取得

withCountメソッド を利用すると「関連モデル」の総数も取得されます。

>>> User::withCount('posts')->toSql()
=> "select `users`.*, 
           (select count(*) from `posts` where `users`.`id` = `posts`.`user_id`) as `posts_count` 
    from `users`"
>>> 
>>> User::withCount('posts')->first()
=> App\Models\User {#2999
     id: 1,
     name: "山岸 くみ子",
     country_id: 5,
     created_at: "2018-11-15 06:38:14",
     updated_at: "2018-11-15 06:38:14",
     posts_count: 4,
   }

上記例では、 posts_count に総数が設定されています。

「関連モデル」をEagerローディング

N+1クエリ問題 の対策として、Eagerローディングする方法を確認します。

withでEagerロード

User::with('posts')->get();
select * from `users`
select * from `posts` where `posts`.`user_id` in ('1', '2', '3')

loadでEagerロード

$users = User::get();
if ($condition) {
    $users->load('posts');
}
select * from `users`
select * from `posts` where `posts`.`user_id` in ('1', '2', '3')

条件ごとに、Eagerロードするリレーションを変更したいケースなどに有効です。

ネストしたEagerロード

Country::with('users.posts')->get();
select * from `countries`
select * from `users` where `users`.`country_id` in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '10')
select * from `posts` where `posts`.`user_id` in ('1', '2', '3')

複数リレーションをEagerロード

User::with(['posts', 'videos'])->get();
select * from `users`
select * from `posts` where `posts`.`user_id` in ('1', '2', '3')
select * from `videos` where `videos`.`user_id` in ('1', '2', '3')

特定条件をつけてEagerロード

User::with(['posts' => function ($query) {
    $query->where('title', 'like', '%first%');
}])->get();
select * from `users`
select * from `posts` where `posts`.`user_id` in ('1', '2', '3') and `title` like '%first%'