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

簡介

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

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

老闆和打工仔的故事

`Eloquent Builder` 是咱們在使用 `Laravel` 
 模型進行查詢的時候調用的對象,轉換 `SQL` 最終是調用了
 `Query Builder` 對象的服務。
 因此咱們將介紹兩個 `Builder` 對象。
複製代碼

Query BuilderIlluminate\Database\Query\Builder閉包

Eloquent BuilderIlluminate\Database\Eloquent\Builderapp

這兩個對象的關係就像老闆和打工仔,上層Eloquent Builder 指揮下層 Query Builder 幹活。ui

查詢 User::find(1)

當咱們執行這條查詢的時候,會觸發 Model 的方法this

這裏無論是否靜態調用都不要緊,最終會轉到 __callspa

public static function __callStatic($method, $parameters)
{
    return (new static)->$method(...$parameters);
}
複製代碼

再轉發到code

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 Buildercdn

咱們來看 $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 這幾個圈起來的屬性賦值

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

上面咱們存好了數據,那麼後面咱們就要想辦法從這些屬性中構建處 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()

其餘功能等待讀者開發!

相關文章
相關標籤/搜索