【Laravel】動的プロパティとリレーションメソッドの違い

はじめに

こんにちは、第2チームにて、管理画面の開発を担当している岡山です。 弊社では、Laravelを使った開発を進めています。 関連するモデルを扱う場面では、リレーションを利用することが多いと思います。 その際、以下のようなコードを書きますが、()の有無による違いで混乱することがあります。

<?php
$user->posts;
$user->posts();

似ていますが、前者は「動的プロパティ」、後者は「リレーションメソッド」と呼ばれ、それぞれ異なる挙動をします。 今回はその違いについて確認していきたいと思います。

目次

前提

ここでは、Userが複数のPostを持つリレーション(1対多)を例に説明します。

User.php

<?php
use Illuminate\Database\Eloquent\Relations\HasMany;

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

Post.php

<?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; 

この場合、$user->postsはIlluminate\Database\Eloquent\Collectionを返します。 LazyLoading(遅延ロード)されるため、プロパティへアクセスした時にデータが読み込まれます。 さらに、EagerLoading(後述)を使用することもできます。

$user->postで発行されるクエリは以下になります。

<?php
[
    "select * from `posts` where `posts`.`user_id` = 1 and `posts`.`user_id` is not null"
]

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

次にリレーションメソッドは以下のように書きます。

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

$user->posts()はIlluminate\Database\Eloquent\Relations\HasManyを返します。 動的プロパティとは異なり、リレーションメソッドはLazy Loadingを行わず、Eager Loadingを使用できません。 さらに、->get()を繋げるとCollectionが返されます。

<?php
$user->posts()->get();

この時、発行されるクエリは$user->postで発行されるクエリと同じです。

<?php
[
    "select * from `posts` where `posts`.`user_id` = 1 and `posts`.`user_id` is not null"
]

クエリビルダとして扱うことができるため、リレーションに加え、絞り込み条件を追加できます。

<?php
$users->posts()->where('is_deleted', false)->get();

この場合、発行されるクエリは次のようになります。先ほどのクエリにis_deletedの条件が追加されていることがわかります。

<?php
[
    "select * from `posts` where `posts`.`user_id` = 1 and `posts`.`user_id` is not null and `is_deleted` = false"
]

EagerLoadingについて

Eager Loadingとは、特定の親モデルを取得する際に、関連するモデルを事前にまとめて読み込む手法です。

実際の例

例えば、複数のユーザーを取得し、その各ユーザーの投稿を取得する処理を考えてみます。 以下のコードでは、何回のクエリが発行されるでしょうか?

<?php
$users = User::whereIn('id', [1, 2, 3, 4, 5])->get();
foreach ($users as $user) {
    // 動的プロパティの場合
    $posts = $user->posts; 
    // リレーションメソッドの場合
    $posts = $user->posts()->get();
}

この場合、動的プロパティとリレーションメソッドのどちらを使っても合計6回のクエリが発行されます。

<?php
[
   "select * from `users` where `id` in (1, 2, 3, 4, 5)",
    "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"
]

このように、特定の親モデルに対して関連モデルを個別に取得する際に、大量のクエリが発行されてしまうのがいわゆるN+1問題です。

この問題を回避する方法として、withメソッドを使用したEager Loadingがあります。 withメソッドは動的プロパティのみで使用できます。 withメソッドを追加したコードは以下のようになります。

<?php
$users = User::with('posts')->whereIn('id', [1, 2, 3, 4, 5])->get();

このコードで実行されるクエリは以下のようになります。

<?php
[
    "select * from `users` where `id` in (1, 2, 3, 4, 5)",
    "select * from `posts` where `posts`.`user_id` in (1, 2, 3, 4, 5)"
]

これにより、クエリの発行回数が6回から2回に減少しました。 クエリの内容を見ると、関連モデルの取得が一括で行われているため、クエリの発行回数を削減できていることがわかります。

まとめ

最後にそれぞれの違いを表にまとめました。

動的プロパティ リレーションメソッド
使用例 $user->posts $user->posts()
返り値 モデルのコレクション リレーションのインスタンス
遅延ロードの有無 する しない
EagerLoadingの可否 可能 不可

コードは()があるかないかだけの違いですが、比較すると全く異なるものであることがわかりました。 普段から自分の書いているコードを深く理解することの重要性を再確認しました。

参考リンク

12.x Eloquent:リレーション Laravel

Eloquent: Relationships - Laravel 12.x - The PHP Framework For Web Artisans

ⓒ i-plug,inc. All Rights Reserved.