Laravel の動的プロパティと Eager Loading について解説します。
下記の表を見て何のことかわからないという人向けです。
動的プロパティ | リレーションメソッド | |
---|---|---|
(例) | $user->posts | $user->posts() |
Lazy Loading (遅延ロード) | する | しない |
Eager Loading | 可能 | 不可能 |
クエリビルダとして動作 | しない | する |
返すオブジェクト | Collection | リレーション |
動的プロパティとは
リレーションをプロパティのようにアクセスできる仕組みです。
(例) User に複数の Post が紐づく「1対多」のリレーションを考えます。
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
↓ これが動的プロパティです。
$user = User::find(1);
$user->posts;
動的プロパティは Illuminate\Database\Eloquent\Collection
を返します。
また,動的プロパティは遅延ロードします。実際にアクセスした時のみデータが読み込まれます。そのため,後で解説する「N+1問題」が発生してしまいます。
リレーションメソッドとは
Eloquent リレーションはクエリビルダとして動作します。
動的プロパティと違い,Lazy Loading はしません。
↓ これがリレーションメソッドです。
$user = User::find(1);
$user->posts(); // Illuminate\Database\Eloquent\Relations\HasMany を返す
$user->posts()->get(); // Illuminate\Database\Eloquent\Collection を返す
リレーションメソッドは Illuminate\Database\Eloquent\Relations\HasMany
を返しています。
get メソッドで繋げると,動的プロパティと同様に Collection を返します。
Eloquent リレーションはクエリビルダとして動作するので,自由にクエリ制約を加えることができます。
$user = User::find(1);
$user->posts()->where('is_published', 1)->get();
N+1問題とは
全 User を取得し,ループで各ユーザーに紐づく Posts にアクセスする状況を考えます。
$users = User::all();
foreach ($users as $user) {
$user->posts;
}
ユーザーが10人の場合,11回のSQLクエリが発行されています。
SELECT * FROM users
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
SELECT * FROM posts WHERE posts.user_id = 6 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 7 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 8 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 9 AND posts.user_id IS NOT NULL
SELECT * FROM posts WHERE posts.user_id = 10 AND posts.user_id IS NOT NULL
見ての通り,クエリが大量に発行されてしまいます。これが「N + 1問題」です。
N+1問題の解決策
withメソッドを使う
$users = User::with(['posts'])->get(); // Illuminate\Database\Eloquent\Collection を返す
dd(User::with(['chats'])->get())
でコレクションの中身を見てみると
各々の User インスタンスの中に,relations
という項目があります。
その中に posts コレクションが格納されています。
with を使わずに dd(User::all())
とすると relations
は空になります。
SQL を見ると,2回しか発行されていないことがわかります。
全 User に紐づく Posts を1つのクエリで取得しています。
SELECT * FROM users
SELECT * FROM posts WHERE posts.user_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9. 10)
with は何を返すのか
dd(User::with(['posts']))
をすると Illuminate\Database\Eloquent\Builder
を返していることがわかります。with メソッドはクエリビルダとして動作しています。
つまり,クエリを追加することも可能です。
// この where は User に対する制約であることに注意
User::with(['posts'])->where('name', 'Alice')->get();
さまざまな Eager Loading
複数の Eager Loading
$users = User::with(['posts', 'articles'])->get();
ネストした Eager Loading
$users = User::with(['posts.comments'])->get();
遅延 Eager Loading
$users = User::all();
if ($someCondition) {
$users->load('posts', 'articles');
}
制約付き Eager Loading
// 公開済み( is_published が1)の Posts に限定する
// 条件に一致する Posts を1つも持たない User も取得される点に注意
$users = User::with(['posts' => function ($query) {
$query->where('is_published', 1);
}])->get();
動的プロパティの仕組み
$user = User::find(1);
$user->posts;
posts()
のように ()
が付いていないので,posts メソッドは呼ばれません。
かと言って posts プロパティが定義されている訳でもありません。
この場合,マジックメソッドの __get
が呼ばれます。
Illuminate\Database\Eloquent\Model
を見てみます。
// 引数 $key にはアクセスしたプロパティ名(ここでは 'posts' )がセットされます
public function __get($key)
{
return $this->getAttribute($key);
}
Illuminate\Database\Eloquent\Model::__get
__get()
はマジックメソッドです。存在しない又はアクセス不能なプロパティにアクセスされた際に呼び出されます。
getAttribute()
は Illuminate\Database\Eloquent\Model.php
には存在しません。
use している HasAttributes トレイトに実装されています。
namespace Illuminate\Database\Eloquent;
abstract class Model
{
use Concerns\HasAttributes,
Concerns\HasRelationships,
}
// ※その他は省略
トレイトはコードを再利用するための仕組みです。機能をまとめたものです。
public function getAttribute($key)
{
if (! $key) {
return;
}
if (array_key_exists($key, $this->attributes) ||
array_key_exists($key, $this->casts) ||
$this->hasGetMutator($key) ||
$this->hasAttributeGetMutator($key) ||
$this->isClassCastable($key)) {
return $this->getAttributeValue($key);
}
if (method_exists(self::class, $key)) {
return;
}
return $this->getRelationValue($key);
}
Illuminate/Database/Eloquent/Concerns/HasAttributes::getAttribute
最後に return してる getRelationValue()
を見てみます。
public function getRelationValue($key)
{
// 既にキー 'posts' が relations 配列に存在すればそれを返す
// 2回目以降はキャッシュを返す
if ($this->relationLoaded($key)) {
return $this->relations[$key];
}
if (! $this->isRelation($key)) {
return;
}
if ($this->preventsLazyLoading) {
$this->handleLazyLoadingViolation($key);
}
return $this->getRelationshipFromMethod($key);
}
Illuminate/Database/Eloquent/Concerns/HasAttributes::getRelationValue
relationLoaded()
を見てみます。
protected $relations = [];
// 'posts' というキーが relations 配列に存在しているかを判定
public function relationLoaded($key)
{
return array_key_exists($key, $this->relations);
}
Illuminate/Database/Eloquent/Concerns/HasRelationships::relationLoaded
最後に return してる getRelationshipFromMethod()
を追います。
// 引数 $method には 'posts' が渡される
protected function getRelationshipFromMethod($method)
{
// $user->posts() の実行結果を $relation に格納
$relation = $this->$method();
if (! $relation instanceof Relation) {
if (is_null($relation)) {
throw new LogicException(sprintf(
'%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method
));
}
throw new LogicException(sprintf(
'%s::%s must return a relationship instance.', static::class, $method
));
}
// $relation->getResults() の結果が $results にセットされる
// クロージャの処理を実行した後,$relation->getResults() の結果が返される
// $relation->getResults() でDBからデータを取得
return tap($relation->getResults(), function ($results) use ($method) {
// 動的プロパティに値をセット
$this->setRelation($method, $results);
});
}
Illuminate/Database/Eloquent/Concerns/HasAttributes::getRelationshipFromMethod
tap
の中にある setRelation()
は, Model.php で use している HasRelationships トレイトにあります。
protected $relations = [];
public function setRelation($relation, $value)
{
// relations['posts'] = 実際の Posts データ
$this->relations[$relation] = $value;
return $this;
}
Illuminate/Database/Eloquent/Concerns/HasRelationships::setRelation
$this->relations[$relation] = $value;
の部分で実際のデータをセットしています。
さいごに
動的プロパティと Eager Loading について解説しました。
間違っている点などありましたら教えてください。