構成
- 第1回 Eloquent ソースコードリーディング - モデルの取得 (この記事)
- 第2回 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 を行う際に発生してしまう以下のようなバグを修正するために,あとから追加された経緯がある。
モデル取得時に何が起こっているか?
定義
<?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 については説明が複雑になるので,次回以降に持ち越す。