【Laravel-海賊王系列】第十六章,Builder 解析
Eloquent Builder` 是我們在使用 `Laravel` 模型進行查詢的時候呼叫的物件,轉換 `SQL` 最終是呼叫了 `Query Builder` 物件的服務。 所以我們將介紹兩個 `Builder` 物件。 複製程式碼
Query Builder
指 Illuminate\Database\Query\Builder
Eloquent Builder
指 Illuminate\Database\Eloquent\Builder
這兩個物件的關係就像老闆和打工仔,上層 Eloquent Builder
指揮下層 Query Builder
幹活。
查詢 User::find(1)
當我們執行這條查詢的時候,會觸發 Model
的方法
這裡不管是否靜態呼叫都沒關係,最終會轉到 __call
public static function __callStatic($method, $parameters) { return (new static)->$method(...$parameters); } 複製程式碼
再轉發到
public function __call($method, $parameters) { // "如果是這兩個方法的話會優先呼叫 Model自身定義的" if (in_array($method, ['increment', 'decrement'])) { return $this->$method(...$parameters); } // "這裡的 $this->newQuery() 就是 Eloquent Builder 物件!" // "轉發呼叫,實際執行了 $this->newQuery()->{$method}" return $this->forwardCallTo($this->newQuery(), $method, $parameters); } 複製程式碼
首先出場的是 Eloquent Builder
我們來看 $this->newQuery()
獲取的是什麼!
public function newQuery() { return $this->registerGlobalScopes($this->newQueryWithoutScopes()); } 複製程式碼
繼續分析 newQueryWithoutScopes()
public function newQueryWithoutScopes() { return $this->newModelQuery() ->with($this->with) ->withCount($this->withCount); } 複製程式碼
public function newModelQuery() { return $this->newEloquentBuilder( $this->newBaseQueryBuilder() )->setModel($this); } 複製程式碼
上面程式碼的目的就是將 Query Builder
物件賦值給 Eloquent Builder
的 $this->query
成員
public function newEloquentBuilder($query) { return new Builder($query); } // "Builder 的構造方法宣告" public function __construct(QueryBuilder $query) { $this->query = $query; } 複製程式碼
返回一個 Query Builder
物件
protected function newBaseQueryBuilder() { $connection = $this->getConnection(); return new QueryBuilder( $connection, $connection->getQueryGrammar(), $connection->getPostProcessor() ); } 複製程式碼
其實經過上面的操作最主要的目的就是將 Query Builder
賦值給 Eloquent Builder
可見 Eloquent Builder
並沒有構建 SQL
語句的能力
但是這層封裝使得 Eloquent Builder
擁有了這能力。
所以真正的構建服務還是來自 Query Builder
物件。
經過上面的分析我們回到最開始的呼叫處
public function __call($method, $parameters) { // "如果是這兩個方法的話會優先呼叫 Model自身定義的" if (in_array($method, ['increment', 'decrement'])) { return $this->$method(...$parameters); } // "這裡的 $this->newQuery() 就是 Eloquent Builder 物件!" // "轉發呼叫,實際執行了 $this->newQuery()->{$method}" return $this->forwardCallTo($this->newQuery(), $method, $parameters); } 複製程式碼
所以我們對模型層的大部分呼叫都是呼叫 (Eloquent Builder)>{$method}
那麼就從開篇的例子開始分析這個 Eloquent Builder
到底有什麼方法!
Find(1)
方法解析
public function find($id, $columns = ['*']) { if (is_array($id) || $id instanceof Arrayable) { return $this->findMany($id, $columns); } return $this->whereKey($id)->first($columns); } 複製程式碼
我們傳入的是一個字串,直接分析 $this->whereKey($id)->first($columns)
public function whereKey($id) { if (is_array($id) || $id instanceof Arrayable) { $this->query->whereIn($this->model->getQualifiedKeyName(), $id); return $this; } // "從這裡開始分析" // "$this->model->getQualifiedKeyName() 就是獲取主鍵的名字是什麼,就不贅述" return $this->where($this->model->getQualifiedKeyName(), '=', $id); } 複製程式碼
️:checkered_flag:繼續看,接下來就是重點了!關於查詢構建器是如何構建 SQL
的。
我們在腦海裡面先幻想一下,查詢構建起是幹啥的?!
回憶下是不是很久沒有寫原生 SQL
了?還記得 SELECT * FROM users WHERE id = 1;
嗎,在 Laravel
中查詢構建器功能就是將我們的 User::find(1)
轉化成上面的 SQL
好了,我們回來繼續分析如何完成這個轉化!
打工仔現身
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; } 複製程式碼
執行這裡的程式碼,這裡呼叫了 打工仔 Query Builder
$this->query->where(...func_get_args()); 複製程式碼
展開打工仔的 where
, 接收的引數就是上面完完整整的轉發了一次。
public function where($column, $operator = null, $value = null, $boolean = 'and') { if (is_array($column)) { return $this->addArrayOfWheres($column, $boolean); } [$value, $operator] = $this->prepareValueAndOperator( $value, $operator, func_num_args() === 2 ); if ($column instanceof Closure) { return $this->whereNested($column, $boolean); } if ($this->invalidOperator($operator)) { [$value, $operator] = [$operator, '=']; } if ($value instanceof Closure) { return $this->whereSub($column, $operator, $value, $boolean); } if (is_null($value)) { return $this->whereNull($column, $boolean, $operator !== '='); } if (Str::contains($column, '->') && is_bool($value)) { $value = new Expression($value ? 'true' : 'false'); } $type = 'Basic'; $this->wheres[] = compact( 'type', 'column', 'operator', 'value', 'boolean' ); if (! $value instanceof Expression) { $this->addBinding($value, 'where'); } return $this; } 複製程式碼
上面這麼一大堆的程式碼實在是懶得講了~看圖吧,
反正就是對 Builder
這幾個圈起來的屬性賦值
仔細看看,反正沒什麼難的,就是先把資料丟到這些成員裡存起來。

上面我們存好了資料,那麼後面我們就要想辦法從這些屬性中構建處 SQL
了,別急,我們現在開始。
執行查詢
回到剛才開始的地方
public function find($id, $columns = ['*']) { if (is_array($id) || $id instanceof Arrayable) { return $this->findMany($id, $columns); } // "剛才執行了這句" return $this->whereKey($id)->first($columns); } 複製程式碼
打工仔兄弟 Illuminate\Database\Concerns\BuildsQueries
現身
這裡的 first()
方法是 use
這 BuildsQueries
這個特質類
public function first($columns = ['*']) { return $this->take(1)->get($columns)->first(); } 複製程式碼
追進去這裡要注意 $this
這裡指向的肯定是 Eloquent Builder
,
原始碼裡面是沒有 take
這個方法,這是又是通過 __call
方法來呼叫
最終執行程式碼就是 $this->query->take(1)->get($columns)->first()
這裡關於為什麼這樣執行的可以查閱 Eloquent Builder
魔術方法。
接著來
public function take($value) { // "就是賦值操作,給物件的 $this->limit = $value;" return $this->limit($value); } 複製程式碼
繼續看,準備好秋名山最後幾個關卡來了
// "這個 `get`方法是老闆 `Eloquent Builder` 中定義的" public function get($columns = ['*']) { $builder = $this->applyScopes(); if (count($models = $builder->getModels($columns)) > 0) { $models = $builder->eagerLoadRelations($models); } return $builder->getModel()->newCollection($models); } 複製程式碼
接著重點是 $builder->getModels($columns)
獲取資料的操作
public function getModels($columns = ['*']) { return $this->model->hydrate( $this->query->get($columns)->all() )->all(); } 複製程式碼
我們不理會其他,只看 $this->query->get($columns)->all()
這裡就是呼叫打工仔 Query Builder
的 get()
public function get($columns = ['*']) { // "onceWithColumns 這個方法沒什麼好分析,接收兩個引數,返回第二個引數(閉包)" return collect($this->onceWithColumns($columns, function () { return $this->processor->processSelect($this, $this->runSelect()); })); } 複製程式碼
繼續看閉包裡面 $this->processor->processSelect($this, $this->runSelect());
public function processSelect(Builder $query, $results) { // "這裡沒幹啥,就是把 $results 返回" return $results; } 複製程式碼
那麼最重點的來了,看名字就是執行 SQL
$this->runSelect() 複製程式碼
我們看到這裡的 select
有三個引數,第一個就是我們苦苦尋找的 SQL
,第二個是 PDO
引數繫結的資料。
這裡的 $this->connection->select()
是驅動層提供對接 MySQL
的呼叫,我們不用關心啦~
protected function runSelect() { return $this->connection->select( $this->toSql(), $this->getBindings(), ! $this->useWritePdo ); } 複製程式碼
toSql()
tips 我們平時在使用 (new User)->getQuery()->toSql();
可以看到預編譯的 SQL
public function toSql() { return $this->grammar->compileSelect($this); } ... // "方便閱讀合併了部分原始碼" public function compileSelect(Builder $query) { if ($query->unions && $query->aggregate) { $column = $this->columnize($aggregate['columns']); if ($query->distinct && $column !== '*') { $column = 'distinct '.$column; } $sql = 'select '.$aggregate['function'].'('.$column.') as aggregate'; $query->aggregate = null; $sql =$sql.' from ('.$this->compileSelect($query).') as '.$this->wrapTable('temp_table'); } $original = $query->columns; if (is_null($query->columns)) { $query->columns = ['*']; } $sql = trim($this->concatenate( $this->compileComponents($query)) ); $query->columns = $original; return $sql; } 複製程式碼
這一坨坨代實在講起來沒有味道,就是各種判斷,然後抽取屬性拼接成字串。
這裡面有興趣可以自行研究,今天主要是將邏輯整理清楚,知道為什麼就夠了!