Laravel核心解讀--Database(二) 查詢構建器

上文咱們說到執行DB::table('users')->get()是由Connection對象執行table方法返回了一個QueryBuilder對象,QueryBuilder提供了一個方便的接口來建立及運行數據庫查詢語句,開發者在開發時使用QueryBuilder不須要寫一行SQL語句就能操做數據庫了,使得書寫的代碼更加的面向對象,更加的優雅。php

class MySqlConnection extends Connection
{
     ......
}

class Connection implements ConnectionInterface
{
    public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])
    {
        $this->pdo = $pdo;

        $this->database = $database;

        $this->tablePrefix = $tablePrefix;

        $this->config = $config;

        $this->useDefaultQueryGrammar();

        $this->useDefaultPostProcessor();
    }
    ......   
    public function table($table)
    {
        return $this->query()->from($table);
    }
    ......
    
    public function query()
    {
        return new QueryBuilder(
            $this, $this->getQueryGrammar(), $this->getPostProcessor()
        );
    }
    ......
    public function useDefaultQueryGrammar()
    {
        $this->queryGrammar = $this->getDefaultQueryGrammar();
    }
    
    protected function getDefaultQueryGrammar()
    {
        return new QueryGrammar;
    }
    
    public function useDefaultPostProcessor()
    {
        $this->postProcessor = $this->getDefaultPostProcessor();
    }
    
    protected function getDefaultPostProcessor()
    {
        return new Processor;
    }
    
    
}

經過上面的代碼段能夠看到Connection類的構造方法裏出了注入了Connector數據庫鏈接器(就是參數裏的$pdo),還加載了兩個重要的組件Illuminate\Database\Query\Grammars\Grammar:SQL語法編譯器實例和Illuminate\Database\Query\Processors\Processor SQL結果處理器實例。 咱們看一下Connection的table方法,它返回了一個QueryBuilder實例, 其在實例化的時候Connection實例、Grammer實例和Processor實例會被做爲參數傳人QueryBuilder的構造方法中。 git

接下咱們到QueryBuilder類文件\Illuminate\Database\Query\Builder.php裏看看它裏面的源碼github

namespace Illuminate\Database\Query;

class Builder
{
    public function __construct(ConnectionInterface $connection,
                                Grammar $grammar = null,
                                Processor $processor = null)
    {
        $this->connection = $connection;
        $this->grammar = $grammar ?: $connection->getQueryGrammar();
        $this->processor = $processor ?: $connection->getPostProcessor();
    }
    
    //設置query目標的table並返回builder實例自身
    public function from($table)
    {
        $this->from = $table;

        return $this;
    }
    
}

QueryBuilder構建SQL參數

下面再來看看where方法裏都執行裏什麼, 爲了方便閱讀咱們假定執行條件where('name', '=', 'James')sql

//class \Illuminate\Database\Query\Builder
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
    //where的參數能夠是一維數組或者二維數組
    //應用一個條件一維數組['name' => 'James']
    //應用多個條件用二維數組[['name' => 'James'], ['age' => '28']]
    if (is_array($column)) {
        return $this->addArrayOfWheres($column, $boolean);
    }

    //當這樣使用where('name', 'James')時,會在這裏把$operator賦值爲"="
    list($value, $operator) = $this->prepareValueAndOperator(
        $value, $operator, func_num_args() == 2 // func_num_args()爲3,3個參數
    );

    // where()也能夠傳閉包做爲參數
    if ($column instanceof Closure) {
        return $this->whereNested($column, $boolean);
    }

    // 若是$operator不合法會默認用戶是想省略"="操做符,而後把原來的$operator賦值給$value
    if ($this->invalidOperator($operator)) {
        list($value, $operator) = [$operator, '='];
    }

    // $value是閉包時,會生成子查詢
    if ($value instanceof Closure) {
        return $this->whereSub($column, $operator, $value, $boolean);
    }

    // where('name')至關於'name' = null做爲過濾條件
    if (is_null($value)) {
        return $this->whereNull($column, $boolean, $operator != '=');
    }

    // $column沒有包含'->'字符
    if (Str::contains($column, '->') && is_bool($value)) {
        $value = new Expression($value ? 'true' : 'false');
    }
    $type = 'Basic';                

    //每次調用where、whereIn、orWhere等方法時都會把column operator和value以及對應的type組成一個數組append到$wheres屬性中去
    //['type' => 'basic', 'column' => 'name', 'operator' => '=', 'value' => 'James', 'boolean' => 'and']
    $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean');

    if (! $value instanceof Expression) {
        // 這裏是把$value添加到where的綁定值中
        $this->addBinding($value, 'where');
    }

    return $this;
}

protected function addArrayOfWheres($column, $boolean, $method = 'where')
{
    return $this->whereNested(function ($query) use ($column, $method, $boolean) {
        foreach ($column as $key => $value) {
            //上面where方法的$column參數爲二維數組時這裏會去遞歸調用where方法
            if (is_numeric($key) && is_array($value)) {
                $query->{$method}(...array_values($value));
            } else {
                $query->$method($key, '=', $value, $boolean);
            }
        }
    }, $boolean);
}

public function whereNested(Closure $callback, $boolean = 'and')
{
    call_user_func($callback, $query = $this->forNestedWhere());

    return $this->addNestedWhereQuery($query, $boolean);
}

//添加執行query時要綁定到query裏的值
public function addBinding($value, $type = 'where')
{
    if (! array_key_exists($type, $this->bindings)) {
        throw new InvalidArgumentException("Invalid binding type: {$type}.");
    }

    if (is_array($value)) {
        $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value));
    } else {
        $this->bindings[$type][] = $value;
    }

    return $this;
}

因此上面DB::table('users')->where('name', '=', 'James')執行後QueryBuilder對象裏的幾個屬性分別有了一下變化:數據庫

public $from = 'users';

public $wheres = [
       ['type' => 'basic', 'column' => 'name', 'operator' => '=', 'value' => 'James', 'boolean' => 'and']
]

public $bindings = [
    'select' => [],
    'join'   => [],
    'where'  => ['James'],
    'having' => [],
    'order'  => [],
    'union'  => [],
];

經過bindings屬性裏數組的key你們應該都能猜到若是執行select、orderBy等方法,那麼這些方法就會把要綁定的值分別append到select和order這些數組裏了,這些代碼我就不貼在這裏了,你們看源碼的時候能夠本身去看一下,下面咱們主要來看一下get方法裏都作了什麼。數組

//class \Illuminate\Database\Query\Builder
public function get($columns = ['*'])
{
    $original = $this->columns;

    if (is_null($original)) {
        $this->columns = $columns;
    }

    $results = $this->processor->processSelect($this, $this->runSelect());

    $this->columns = $original;

    return collect($results);
}

protected function runSelect()
{
    return $this->connection->select(
        $this->toSql(), $this->getBindings(), ! $this->useWritePdo
    );
}

public function toSql()
{
    return $this->grammar->compileSelect($this);
}

//將bindings屬性的值轉換爲一維數組
public function getBindings()
{
    return Arr::flatten($this->bindings);
}

在執行get方法後,QueryBuilder首先會利用grammar實例編譯SQL語句並執行,而後利用Processor實例處理結果集,最後返回通過處理後的結果集。 咱們接下來看下這兩個流程。閉包

Grammar將構建的SQL參數編譯成SQL語句

咱們接着從toSql()方法開始接着往下看Grammar類app

public function toSql()
{
    return $this->grammar->compileSelect($this);
}

/**
 * 將Select查詢編譯成SQL語句
 * @param  \Illuminate\Database\Query\Builder  $query
 * @return string
 */
public function compileSelect(Builder $query)
{
    
    $original = $query->columns;
    //若是沒有QueryBuilder裏沒制定查詢字段,那麼默認將*設置到查詢字段的位置
    if (is_null($query->columns)) {
        $query->columns = ['*'];
    }
    //遍歷查詢的每一部份,若是存在就執行對應的編譯器來編譯出那部份的SQL語句
    $sql = trim($this->concatenate(
        $this->compileComponents($query))
    );

    $query->columns = $original;

    return $sql;
}

/**
 * 編譯Select查詢語句的各個部分
 * @param  \Illuminate\Database\Query\Builder  $query
 * @return array
 */
protected function compileComponents(Builder $query)
{
    $sql = [];

    foreach ($this->selectComponents as $component) {
        //遍歷查詢的每一部份,若是存在就執行對應的編譯器來編譯出那部份的SQL語句
        if (! is_null($query->$component)) {
            $method = 'compile'.ucfirst($component);

            $sql[$component] = $this->$method($query, $query->$component);
        }
    }

    return $sql;
}

/**
 * 構成SELECT語句的各個部分
 * @var array
 */
protected $selectComponents = [
    'aggregate',
    'columns',
    'from',
    'joins',
    'wheres',
    'groups',
    'havings',
    'orders',
    'limit',
    'offset',
    'unions',
    'lock',
];

在Grammar中,將SELECT語句分紅來不少單獨的部分放在了$selectComponents屬性裏,執行compileSelect時程序會檢查QueryBuilder設置了$selectComponents裏的哪些屬性,而後執行已設置屬性的編譯器編譯出每一部分的SQL來。
仍是用咱們以前的例子DB::table('users')->where('name', 'James')->get(),在這個例子中QueryBuilder分別設置了cloums(默認*)、fromwheres屬性,那麼咱們見先來看看這三個屬性的編譯器:函數

/**
 * 編譯Select * 部分的SQL
 * @param  \Illuminate\Database\Query\Builder  $query
 * @param  array  $columns
 * @return string|null
 */
protected function compileColumns(Builder $query, $columns)
{
    // 若是SQL中有聚合,那麼SELECT部分的編譯教給aggregate部分的編譯器去處理
    if (! is_null($query->aggregate)) {
        return;
    }

    $select = $query->distinct ? 'select distinct ' : 'select ';

    return $select.$this->columnize($columns);
}

//將QueryBuilder $columns字段數組轉換爲字符串
public function columnize(array $columns)
{    
    //爲每一個字段調用Grammar的wrap方法
    return implode(', ', array_map([$this, 'wrap'], $columns));
}

compileColumns執行完後compileComponents裏的變量$sql的值會變成['columns' => 'select * '] 接下來看看fromwheres部分post

protected function compileFrom(Builder $query, $table)
{
    return 'from '.$this->wrapTable($table);
}

/**
 * Compile the "where" portions of the query.
 *
 * @param  \Illuminate\Database\Query\Builder  $query
 * @return string
 */
protected function compileWheres(Builder $query)
{
    if (is_null($query->wheres)) {
        return '';
    }
    //每一種where查詢都有它本身的編譯器函數來建立SQL語句,這幫助保持裏代碼的整潔和可維護性
    if (count($sql = $this->compileWheresToArray($query)) > 0) {
        return $this->concatenateWhereClauses($query, $sql);
    }

    return '';
}

protected function compileWheresToArray($query)
{
    return collect($query->wheres)->map(function ($where) use ($query) {
        //對於咱們的例子來講是 'and ' . $this->whereBasic($query, $where)  
        return $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where);
    })->all();
}

每一種where查詢(orWhere, WhereIn......)都有它本身的編譯器函數來建立SQL語句,這幫助保持裏代碼的整潔和可維護性. 上面咱們說過在執行DB::table('users')->where('name', 'James')->get()時$wheres屬性裏的值是:

public $wheres = [
       ['type' => 'basic', 'column' => 'name', 'operator' => '=', 'value' => 'James', 'boolean' => 'and']
]

在compileWheresToArray方法裏會用$wheres中的每一個數組元素去回調執行閉包,在閉包裏:

$where = ['type' => 'basic', 'column' => 'name', 'operator' => '=', 'value' => 'James', 'boolean' => 'and']

而後根據type值把$where和QeueryBuilder做爲參數去調用了Grammar的whereBasic方法:

protected function whereBasic(Builder $query, $where)
{
    $value = $this->parameter($where['value']);

    return $this->wrap($where['column']).' '.$where['operator'].' '.$value;
}

public function parameter($value)
{
    return $this->isExpression($value) ? $this->getValue($value) : '?';
}

whereBasic的返回爲字符串'where name = ?', compileWheresToArray方法的返回值爲:

['and where name = ?']

而後經過concatenateWhereClauses方法將compileWheresToArray返回的數組拼接成where語句'where name = ?'

protected function concatenateWhereClauses($query, $sql)
{
    $conjunction = $query instanceof JoinClause ? 'on' : 'where';
    //removeLeadingBoolean 會去掉SQL裏首個where條件前面的邏輯運算符(and 或者 or)
    return $conjunction.' '.$this->removeLeadingBoolean(implode(' ', $sql));
}

因此編譯完fromwheres部分後compileComponents方法裏返回的$sql的值會變成

['columns' => 'select * ', 'from' => 'users', 'wheres' => 'where name = ?']

而後在compileSelect方法裏將這個由查查詢語句裏每部份組成的數組轉換成真正的SQL語句:

protected function concatenate($segments)
{
    return implode(' ', array_filter($segments, function ($value) {
        return (string) $value !== '';
    }));
}

獲得'select * from uses where name = ?'. toSql執行完了流程再回到QueryBuilder的runSelect裏:

protected function runSelect()
{
    return $this->connection->select(
        $this->toSql(), $this->getBindings(), ! $this->useWritePdo
    );
}

Connection執行SQL語句

$this->getBindings()會獲取要綁定到SQL語句裏的值, 而後經過Connection實例的select方法去執行這條最終的SQL

public function select($query, $bindings = [], $useReadPdo = true)
{
    return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
        if ($this->pretending()) {
            return [];
        }

        $statement = $this->prepared($this->getPdoForSelect($useReadPdo)
                          ->prepare($query));

        $this->bindValues($statement, $this->prepareBindings($bindings));

        $statement->execute();

        return $statement->fetchAll();
    });
}

protected function run($query, $bindings, Closure $callback)
{
    $this->reconnectIfMissingConnection();

    $start = microtime(true);

    try {
        $result = $this->runQueryCallback($query, $bindings, $callback);
    } catch (QueryException $e) {
        //捕獲到QueryException試着重連數據庫再執行一次SQL
        $result = $this->handleQueryException(
            $e, $query, $bindings, $callback
        );
    }
    //記錄SQL執行的細節
    $this->logQuery(
        $query, $bindings, $this->getElapsedTime($start)
    );

    return $result;
}

protected function runQueryCallback($query, $bindings, Closure $callback)
{
    try {
        $result = $callback($query, $bindings);
    }

    //若是執行錯誤拋出QueryException異常, 異常會包含SQL和綁定信息
    catch (Exception $e) {
        throw new QueryException(
            $query, $this->prepareBindings($bindings), $e
        );
    }

    return $result;
}

在Connection的select方法裏會把sql語句和綁定值傳入一個閉包並執行這個閉包:

function ($query, $bindings) use ($useReadPdo) {
        if ($this->pretending()) {
            return [];
        }

        $statement = $this->prepared($this->getPdoForSelect($useReadPdo)
                          ->prepare($query));

        $this->bindValues($statement, $this->prepareBindings($bindings));

        $statement->execute();

        return $statement->fetchAll();
});

直到getPdoForSelect這個階段Laravel纔會鏈接上Mysql數據庫:

protected function getPdoForSelect($useReadPdo = true)
{
    return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
}

public function getPdo()
{
    //若是尚未鏈接數據庫,先調用閉包鏈接上數據庫
    if ($this->pdo instanceof Closure) {
        return $this->pdo = call_user_func($this->pdo);
    }

    return $this->pdo;
}

咱們在上一篇文章裏講過構造方法裏$this->pdo = $pdo;這個$pdo參數是一個包裝裏Connector的閉包:

function () use ($config) {
    return $this->createConnector($config)->connect($config);
};

因此在getPdo階段纔會執行這個閉包根據數據庫配置建立鏈接器來鏈接上數據庫並返回PDO實例。接下來的prepare、bindValues以及最後的execute和fetchAll返回結果集實際上都是經過PHP原生的PDO和PDOStatement實例來完成的。

經過梳理流程咱們知道:

  1. Laravel是在第一次執行SQL前去鏈接數據庫的,之因此$pdo一開始是一個閉包由於閉包會保存建立閉包時的上下文裏傳遞給閉包的變量,這樣就能延遲加載,在用到鏈接數據庫的時候再去執行這個閉包連上數據庫。
  2. 在程序中判斷SQL是否執行成功最準確的方法是經過捕獲QueryException異常

Processor後置處理結果集

processor是用來對SQL執行結果進行後置處理的,默認的processor的processSelect方法只是簡單的返回告終果集:

public function processSelect(Builder $query, $results)
{
    return $results;
}

以後在QueryBuilder的get方法裏將結果集轉換成了Collection對象返回給了調用者.

到這裏QueryBuilder大致的流程就梳理完了,雖然咱們只看了select一種操做但其實其餘的update、insert、delete也是同樣先由QueryBuilder編譯完成SQL最後由Connection實例去執行而後返回結果,在編譯的過程當中QueryBuilder也會幫助咱們進行防SQL注入。

本文已經收錄在系列文章Laravel源碼學習裏,歡迎訪問閱讀。

相關文章
相關標籤/搜索