12
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LaravelAdvent Calendar 2021

Day 23

[Laravel] 動的プロパティと Eager Loading

Last updated at Posted at 2021-12-23

Laravel の動的プロパティと Eager Loading について解説します。
下記の表を見て何のことかわからないという人向けです。

動的プロパティ リレーションメソッド
(例) $user->posts $user->posts()
Lazy Loading (遅延ロード) する しない
Eager Loading 可能 不可能
クエリビルダとして動作 しない する
返すオブジェクト Collection リレーション

動的プロパティとは

リレーションをプロパティのようにアクセスできる仕組みです。

(例) User に複数の Post が紐づく「1対多」のリレーションを考えます。

User.php
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}
Post.php
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 を見てみます。

Illuminate\Database\Eloquent\Model.php
// 引数 $key にはアクセスしたプロパティ名(ここでは 'posts' )がセットされます
public function __get($key)
{
    return $this->getAttribute($key);
}

Illuminate\Database\Eloquent\Model::__get

__get() はマジックメソッドです。存在しない又はアクセス不能なプロパティにアクセスされた際に呼び出されます。

getAttribute()Illuminate\Database\Eloquent\Model.php には存在しません。
use している HasAttributes トレイトに実装されています。

Illuminate\Database\Eloquent\Model.php
namespace Illuminate\Database\Eloquent;

abstract class Model 
{
    use Concerns\HasAttributes,
        Concerns\HasRelationships,
}
// ※その他は省略

この部分で use しています

トレイトはコードを再利用するための仕組みです。機能をまとめたものです。

Illuminate/Database/Eloquent/Concerns/HasAttributes.php
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() を見てみます。

Illuminate/Database/Eloquent/Concerns/HasAttributes.php
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() を見てみます。

Illuminate/Database/Eloquent/Concerns/HasRelationships.php
protected $relations = [];

// 'posts' というキーが relations 配列に存在しているかを判定
public function relationLoaded($key)
{
    return array_key_exists($key, $this->relations);
}

Illuminate/Database/Eloquent/Concerns/HasRelationships::relationLoaded

最後に return してる getRelationshipFromMethod() を追います。

Illuminate/Database/Eloquent/Concerns/HasAttributes.php
// 引数 $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 トレイトにあります。

Illuminate/Database/Eloquent/Concerns/HasRelationships.php
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 について解説しました。
間違っている点などありましたら教えてください。

12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?