【Laravel-海賊王系列】第十六章,Builder 解析

Jijilin發表於2019-03-12

簡介

Builder 就是查詢到 SQL 轉換的紐帶!

這章很難,勸你放棄閱讀 ?‍♀️?‍♂️

老闆和打工仔的故事

 `Eloquent Builder` 是我們在使用 `Laravel` 
 模型進行查詢的時候呼叫的物件,轉換 `SQL` 最終是呼叫了
 `Query Builder` 物件的服務。
 所以我們將介紹兩個 `Builder` 物件。
複製程式碼

Query BuilderIlluminate\Database\Query\Builder

Eloquent BuilderIlluminate\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);
}
複製程式碼
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);
}
複製程式碼

我們傳入的是一個 Int,直接分析 $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);
}
複製程式碼

️?繼續看,接下來就是重點了!關於查詢構建器是如何構建 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 這幾個圈起來的屬性賦值

仔細看看,反正沒什麼難的,就是先把資料丟到這些成員裡存起來。

【Laravel-海賊王系列】第十六章,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() 方法是 useBuildsQueries 這個特質類

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 Builderget()

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()
複製程式碼

這裡的 $this->connection->select() 是驅動層提供對接 MySQL 的呼叫,我們不用關心啦~我們看到這裡的 select 有三個引數,第一個就是我們苦苦尋找的 SQL,第二個是 PDO 引數繫結的資料。

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;
}
複製程式碼

這一坨坨代實在講起來沒有味道,就是各種判斷,然後抽取屬性拼接成字串。

這裡面有興趣可以自行研究,這篇僅僅介紹執行邏輯。

總結

老闆 Eloquent Builder 和打工仔 Query Builder 的職責!

Builder 的原理,先存入屬性,在執行 toSql()

其他功能等待讀者開發!

相關文章