はじめに
こんにちは、第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の可否 | 可能 | 不可 |
コードは()があるかないかだけの違いですが、比較すると全く異なるものであることがわかりました。 普段から自分の書いているコードを深く理解することの重要性を再確認しました。
参考リンク
Eloquent: Relationships - Laravel 12.x - The PHP Framework For Web Artisans