Help us understand the problem. What is going on with this article?

【Laravel】 第1回 Eloquent ソースコードリーディング - モデルの取得

構成

予備知識

処理委譲の仕組み

Eloquent では,以下のような構造化によって,上位オブジェクトがよりレイヤーの低い処理に関しては下位オブジェクトに委譲する形を採っている。上位オブジェクトは __call() を通じて下位オブジェクトのメソッドを呼ぶことができる。

名称 完全修飾名 委譲先 概要
Query Builder Query\Builder - 生のクエリビルダ。
Eloquent Builder Eloquent\Builder Query Builder Query Builder と Model を内包するクエリビルダ。
スコープや Eager Loading の設定はこのレイヤで保持される。
Relation Eloquent\Relations\Relation Eloquent Builder
Query Builder
Eloquent Builder と Model を内包するリレーション。
取得時にどのような条件を付与するか,
取得結果をどのようにモデルにセットするかが定義される。
Model Eloquent\Model Eloquent Builder
Query Builder
(Relation)
モデル。Active Record としても Repository としても機能する。
  • 完全修飾名の \Illuminate\Database は省略している。
  • Model は __call() を通じて Relation に委譲を行うわけではなく,使用者による明示的なリレーションメソッド定義を通じて委譲が定義される。

処理委譲の仕組み

Eloquent Builder のインスタンス生成

Model には newQuery() newQueryWithoutScopes() newModelQuery() などたくさんの似た名前のメソッドが実装されているが,これらが何を行っているかを簡潔にまとめる。

メソッド名 利用するメソッド 概要
newBaseQueryBuilder - Query Builder の生成。
newEloquentBuilder - Eloquent Builder の生成。
引数として渡されてきた Query Builder がセットされる。
newModelQuery newBaseQueryBuilder
newEloquentBuilder
Model インスタンスを持つ Eloquent Builder の生成。
setModel($this) で自身がセットされる。
newQueryWithoutScopes newModelQuery モデルクエリに更に $with $withCount による
規定の Eager Loading のための設定を追加する。
newQuery newQueryWithoutScopes 規定の Eager Loading のための設定を追加したモデルクエリに,
registerGlobalScopes() でグローバルスコープを追加する。
newQueryWithoutRelationships newModelQuery
(に相当する記述)
モデルクエリに $with $withCount の情報を含めないまま,
registerGlobalScopes() でグローバルスコープを追加する。

ビルダーメソッドの関係

一般的に利用者が意識するのは newModelQuery() newQueryWithoutScopes() newQuery() ぐらいで問題ない。 newQueryWithoutRelationships() に関しては, Eager Loading を行う際に発生してしまう以下のようなバグを修正するために,あとから追加された経緯がある。

Model::$withCount for polymorphic models + Eloquent\Builder::where(\Closure) triggers unexpected bound parameter mismatching · Issue #24209 · laravel/framework

モデル取得時に何が起こっているか?

定義

app/User.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
}

ここで,

$user = User::find(1);

を実行したときに何が起こっているかを徹底解説しよう。

Model インスタンスの生成

一見静的なメソッド呼び出しを行っているように思えるが, __callStatic() ですぐインスタンスが生成されている

    /**
     * Handle dynamic static method calls into the method.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public static function __callStatic($method, $parameters)
    {
        return (new static)->$method(...$parameters);
    }

つまり以下の呼び出しは等価である。

User::find(1)
(new User())->find(1)

Eloquent においては,実質ほぼすべての処理がインスタンスメソッドとして実行される。CakePHP などにおいては「テーブル」と「行」が明確に区別されているが,Laravel においては簡略化のためか,どちらも同じ Model クラス上で取り扱われる。Model のインスタンスは id を持っている場合と持っていない場合があるのだ。

これを踏まえた上で,次の処理を見ていこう。

Eloquent Builder の生成と委譲

find() メソッドは Model には実装されていないので, ここから更に __call() を通じて Eloquent Builder のメソッド呼び出しが行われる

    /**
     * Handle dynamic method calls into the model.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        if (in_array($method, ['increment', 'decrement'])) {
            return $this->$method(...$parameters);
        }

        return $this->forwardCallTo($this->newQuery(), $method, $parameters);
    }

forwardCallTo() は先ほどから述べている委譲を行うためのメソッドで,その実態は Support パッケージにある ForwardsCalls トレイトに実装されている

さて,ここで上述の newQuery() が使用されている。このメソッドは,新しい Eloquent Builder を作成し,それに $this をモデル情報として付与し, $with$withCount を規定の Eager Loading 設定として追加し,さらに論理削除などのグローバルスコープを付与したものを返すメソッドだ。

    /**
     * Get a new query builder for the model's table.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function newQuery()
    {
        return $this->registerGlobalScopes($this->newQueryWithoutScopes());
    }
    /**
     * Get a new query builder that doesn't have any global scopes.
     *
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newQueryWithoutScopes()
    {
        return $this->newModelQuery()
                    ->with($this->with)
                    ->withCount($this->withCount);
    }
    /**
     * Get a new query builder that doesn't have any global scopes or eager loading.
     *
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newModelQuery()
    {
        return $this->newEloquentBuilder(
            $this->newBaseQueryBuilder()
        )->setModel($this);
    }

Eloquent Builder での処理および Query Builder への委譲

このインスタンスに find() を呼ぶ処理が委譲される。では, find() の実装 を見てみよう。

    /**
     * Find a model by its primary key.
     *
     * @param  mixed  $id
     * @param  array  $columns
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|static[]|static|null
     */
    public function find($id, $columns = ['*'])
    {
        if (is_array($id) || $id instanceof Arrayable) {
            return $this->findMany($id, $columns);
        }

        return $this->whereKey($id)->first($columns);
    }

単一IDと複数IDで処理が分岐されているが,今回は単一IDなので下部の処理が実行される。 続いてwhereKey() の実装を見てみよう。

    /**
     * Add a where clause on the primary key to the query.
     *
     * @param  mixed  $id
     * @return $this
     */
    public function whereKey($id)
    {
        if (is_array($id) || $id instanceof Arrayable) {
            $this->query->whereIn($this->model->getQualifiedKeyName(), $id);

            return $this;
        }

        return $this->where($this->model->getQualifiedKeyName(), '=', $id);
    }

また単一IDと複数IDの分岐があるが,同じく下部を見よう。ここで setModel() でセットされた $this->model を使って getQualifiedKeyName() が呼び出されている。このメソッドは完全修飾されたモデルに対応するテーブルのキー名を返す。具体的には `users`.`id` という文字列が返っているはずだ。さらに where() の実装を追ってみよう。

    /**
     * Add a basic where clause to the query.
     *
     * @param  string|array|\Closure  $column
     * @param  mixed   $operator
     * @param  mixed   $value
     * @param  string  $boolean
     * @return $this
     */
    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        if ($column instanceof Closure) {
            $column($query = $this->model->newModelQuery());

            $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
        } else {
            $this->query->where(...func_get_args());
        }

        return $this;
    }

今回はクロージャを1つ渡す使い方ではなく,「キー」「演算子」「値」の3つを渡す使い方を採っているので, 分岐の後者が実行される。ここで Query Builder への委譲が行われる。メソッド名が完全に被ってしまっているため, __call() を使用せずに明示的に呼び出しているようだ。

Query Builder の where() の実装は長いため,ここでの説明は割愛する。余力がある人は追ってみて欲しい。最終的に $wheres というプロパティに WHERE `users`.`id` = 1 という絞り込み条件がセットされる。

さて, ->whereKey($id)->first() と処理が続いていたが, fisrt() の実装を見てみよう。このメソッドは BuildsQueries トレイト上にある。

    /**
     * Execute the query and get the first result.
     *
     * @param  array  $columns
     * @return \Illuminate\Database\Eloquent\Model|object|static|null
     */
    public function first($columns = ['*'])
    {
        return $this->take(1)->get($columns)->first();
    }

take() は Query Builder 上にあり, __call() による委譲で呼ばれている。ここで LIMIT 1 が付与される。

    /**
     * Alias to set the "limit" value of the query.
     *
     * @param  int  $value
     * @return \Illuminate\Database\Query\Builder|static
     */
    public function take($value)
    {
        return $this->limit($value);
    }

get() は Eloquent Builder 上に実装されている。 applyScopes() で付与されているスコープの WHERE 条件を付与し, getModels() を更に呼んでいる。1件以上取れた場合のみ Eager Loading を続けて行っている。

    /**
     * Execute the query as a "select" statement.
     *
     * @param  array  $columns
     * @return \Illuminate\Database\Eloquent\Collection|static[]
     */
    public function get($columns = ['*'])
    {
        $builder = $this->applyScopes();

        // If we actually found models we will also eager load any relationships that
        // have been specified as needing to be eager loaded, which will solve the
        // n+1 query issue for the developers to avoid running a lot of queries.
        if (count($models = $builder->getModels($columns)) > 0) {
            $models = $builder->eagerLoadRelations($models);
        }

        return $builder->getModel()->newCollection($models);
    }
    /**
     * Get the hydrated models without eager loading.
     *
     * @param  array  $columns
     * @return \Illuminate\Database\Eloquent\Model[]|static[]
     */
    public function getModels($columns = ['*'])
    {
        return $this->model->hydrate(
            $this->query->get($columns)->all()
        )->all();
    }

$this->query->get($columns)->all() によって, Query Builder がクエリを実行し, Base Collection として取得された結果を配列に変換。更に, $this->model->hydrate(...)->all() によって, stdClass の配列を Model の配列に変換している。 そして最終的に Model の配列を内包する Eloquent Collection が返されている。

(配列 ⇔ コレクション の変換無駄に多すぎないか…?ww)

Eager Loading については説明が複雑になるので,次回以降に持ち越す。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした