Laravel 5.3之 Query Builder 源碼解析(中)

說明:本篇主要學習數據庫鏈接階段和編譯SQL語句部分相關源碼。實際上,上篇已經聊到Query Builder經過鏈接工廠類ConnectionFactory構造出了MySqlConnection實例(假設驅動driver是mysql),在該MySqlConnection中主要有三件利器:\Illuminate\Database\MysqlConnector;\Illuminate\Database\Query\Grammars\Grammar;\Illuminate\Database\Query\Processors\Processor,其中\Illuminate\Database\MysqlConnector是在ConnectionFactory中構造出來的並經過MySqlConnection的構造參數注入的,上篇中重點談到的經過createPdoResolver($config)獲取到的閉包函數做爲參數注入到該MySqlConnection,而\Illuminate\Database\Query\Grammars\Grammar\Illuminate\Database\Query\Processors\Processor是在MySqlConnection構造函數中經過setter注入的。php

開發環境:Laravel5.3 + PHP7mysql

數據庫鏈接器

鏈接工廠類ConnectionFactory中經過簡單工廠方法實例化了MySqlConnection,看下該connection的構造函數:laravel

public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])
    {
        // 該$pdo就是鏈接工廠類createPdoResolver($config)獲得的閉包
        $this->pdo = $pdo;

        // $database就是config/database.php中設置的connections.mysql.database字段,默認爲homestead
        $this->database = $database;

        $this->tablePrefix = $tablePrefix;

        $this->config = $config;

        $this->useDefaultQueryGrammar();

        $this->useDefaultPostProcessor();
    }
    
    public function useDefaultQueryGrammar()
    {
        $this->queryGrammar = $this->getDefaultQueryGrammar();
    }
    
    protected function getDefaultQueryGrammar()
    {
        return new \Illuminate\Database\Query\Grammars\Grammar;
    }
    
    public function useDefaultPostProcessor()
    {
        $this->postProcessor = $this->getDefaultPostProcessor();
    }
    
    protected function getDefaultPostProcessor()
    {
        return new \Illuminate\Database\Query\Processors\Processor;
    }

經過構造函數知道該MySqlConnection有了三件利器:PDO實例;Grammar SQL語法編譯器實例;Processor SQL結果處理器實例。那PDO實例是如何獲得的呢?再看下鏈接工廠類的createPdoResolver($config)方法源碼:sql

protected function createPdoResolver(array $config)
    {
        return function () use ($config) {
            // 等同於(new MySqlConnector)->connect($config)
            return $this->createConnector($config)->connect($config);
        };
    }

閉包裏的代碼這裏尚未執行,是在後續執行SQL語句時調用Connection::select()執行的,以前的Laravel版本是沒有封裝在閉包裏而是先執行了鏈接操做,Laravel5.3是封裝在了閉包裏等着執行SQL語句再鏈接操做,應該是爲了提升效率。不過,這裏先看下其鏈接操做的源碼,假設是先執行了鏈接操做:數據庫

public function connect(array $config)
    {
        // database.php中沒有配置'unix_socket',則調用getHostDsn(array $config)函數
        // $dsn = 'mysql:host=127.0.0.1;port=21;dbname=homestead',假設database.php中是默認配置
        $dsn = $this->getDsn($config);

        // 若是配置了'options',假設沒有配置
        $options = $this->getOptions($config);

        // 建立一個PDO實例
        $connection = $this->createConnection($dsn, $config, $options);

        // 至關於PDO::exec("use homestead;")
        if (! empty($config['database'])) {
            $connection->exec("use `{$config['database']}`;");
        }
        
        $collation = $config['collation'];
        
        // 至關於PDO::prepare("set names utf8 collate utf8_unicode_ci")->execute()
        if (isset($config['charset'])) {
            $charset = $config['charset'];

            $names = "set names '{$charset}'".
                (! is_null($collation) ? " collate '{$collation}'" : '');

            $connection->prepare($names)->execute();
        }
        
        // 至關於PDO::prepare("set time_zone UTC+8")
        if (isset($config['timezone'])) {
            $connection->prepare(
                'set time_zone="'.$config['timezone'].'"'
            )->execute();
        }

        // 假設'modes','strict'沒有設置
        $this->setModes($connection, $config);

        return $connection;
    }
    
    protected function getHostDsn(array $config)
    {
        // 使用extract()函數來讀取一個關聯數組,如['host' => '127.0.0.1', 'database' => 'homestead']
        // 則 $host = '127.0.0.1', $database = 'homestead', 很巧妙的一個函數
        extract($config, EXTR_SKIP);

        return isset($port)
                        ? "mysql:host={$host};port={$port};dbname={$database}"
                        : "mysql:host={$host};dbname={$database}";
    }

經過構造函數知道最重要的一個方法是createConnection($dsn, $config, $options),該方法實例化了一個PDO這裏就明白了Query Builder也只是在PDO基礎上封裝的一層API集合,Query Builder提供的Fluent API使得不須要寫一行SQL語句就能操做數據庫了,使得書寫的代碼更加的面向對象,更加的優美。看下其源碼:api

public function createConnection($dsn, array $config, array $options)
    {
        $username = Arr::get($config, 'username');

        $password = Arr::get($config, 'password');

        try {
            // 抓取出用戶名和密碼,直接new一個PDO實例
            $pdo = $this->createPdoConnection($dsn, $username, $password, $options);
        } catch (Exception $e) {
            $pdo = $this->tryAgainIfCausedByLostConnection(
                $e, $dsn, $username, $password, $options
            );
        }

        return $pdo;
    }
    
    protected function createPdoConnection($dsn, $username, $password, $options)
    {
        // 若是安裝了Doctrine\DBAL\Driver\PDOConnection模塊,就用這個類來實例化出一個PDO
        if (class_exists(PDOConnection::class)) {
            return new PDOConnection($dsn, $username, $password, $options);
        }

        return new PDO($dsn, $username, $password, $options);
    }

總之,經過上面的代碼拿到了MySqlConnection對象,而且該對象有三件利器:PDO;Grammar;ProcessorGrammar將會把Query Builder的fluent api編譯成SQLPDO編譯執行該SQL語句獲得結果集resultsProcessor將會處理該結果集results。OK,那Query Builder是如何把書寫的api編譯成SQL呢?數組

編譯API成SQL

仍是以上篇說到的一行簡單的fluent api爲例:閉包

Route::get('/query_builder', function() {
    // Query Builder
    // (new MySqlConnection)->table('users')->where('id', '=', 1)->get();
    return DB::table('users')->where('id', '=', 1)->get();
});

這裏已經拿到了MySqlConnection對象,看下其table()的源碼:socket

public function table($table)
    {
        return $this->query()->from($table);
    }
    
    public function query()
    {
        return new \Illuminate\Database\Query\Builder(
            $this, $this->getQueryGrammar(), $this->getPostProcessor()
        );
    }
    
    // SQL語法編譯器
    public function getQueryGrammar()
    {
        return $this->queryGrammar;
    }
    
    // 後置處理器
    public function getPostProcessor()
    {
        return $this->postProcessor;
    }

很容易知道Query Builder提供的fluent api都是在Builder這個類裏,上篇也說過這是個很是重要的類。該Builder還必須裝載兩個神器:Grammar SQL語法編譯器;Processor SQL結果集後置處理器。看下Builder類的from()方法:ide

public function from($table)
    {
        $this->from = $table;

        return $this;
    }

只是簡單的賦值給$from屬性,並返回Builder對象,這樣就能夠實現fluent api。OK,看下where('id', '=', 1)的源碼:

public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        // 從這裏也可看出where()語句能夠這樣使用:
        // where(['id' => 1]) 
        // where([
        //   ['name', '=', 'laravel'],
        //   ['status', '=', 'active'],
        // ])
        if (is_array($column)) {
            return $this->addArrayOfWheres($column, $boolean);
        }

        // $value = 1, $operator = '=',這裏可看出若是這麼寫where('id', 1)也能夠
        // 由於prepareValueAndOperator會把第二個參數1做爲$value,並給$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);
        }

        // 檢查操做符是否非法
        if (! in_array(strtolower($operator), $this->operators, true) &&
            ! in_array(strtolower($operator), $this->grammar->getOperators(), true)) {
            list($value, $operator) = [$operator, '='];
        }

        // 這裏$value = 1,不是閉包
        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 != '=');
        }

        $type = 'Basic';

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

        // $wheres = [
        //   ['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and'],
        // ];
        // 因此若是多個where語句如where('id', '=', 1)->where('status', '=', 'active'),則依次在$wheres中註冊:
        // $wheres = [
        //   ['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and'],
        //   ['type' => 'basic', 'column' => 'status', 'operator' => '=', 'value' => 'active', 'boolean' => 'and'],
        // ];
        $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean');

        if (! $value instanceof Expression) {
            // 這裏是把$value與'where'標記符綁定在該Builder的$bindings屬性中
            // 這時,$bindings = [
            //    'where' => [1],
            // ];
            $this->addBinding($value, 'where');
        }

        // 最後返回該Query Builder對象
        return $this;
    }

從Builder類中where('id', '=', 1)的源碼中可看出,重點就是把where()中的變量值按照$column, $operator, $value拆解並裝入$wheres[ ]屬性中,而且$wheres[ ]是一個'table'結構,若是有多個where過濾器,就在$wheres[ ]中按照'table'結構存儲,如[['id', '=', '1'], ['name', '=', 'laravel'], ...]。而且,在$bindings[]屬性中把where過濾器與值相互綁定存儲,若是有多個where過濾器,就相似這樣綁定,['where' => [1, 'laravel', ...], ...]

OK,再看下最後的get()的源碼:

public function get($columns = ['*'])
    {
        $original = $this->columns;

        if (is_null($original)) {
            // $this->columns = ['*']
            $this->columns = $columns;
        }
        
        // processSelect()做爲後置處理器處理query操做後的結果集
        $results = $this->processor->processSelect($this, $this->runSelect());

        $this->columns = $original;

        return collect($results);
    }

從上面的源碼可看出重點有兩步:一是runSelect()編譯執行SQL;二是後置處理器processor處理query操做後的結果集。說明runSelect()方法幹了兩件大事:編譯API爲SQL;執行SQL。在看下這兩步驟以前,先看下後置處理器對查詢的結果集作了什麼後置操做:

// \Illuminate\Database\Query\Processors\Processor
    public function processSelect(Builder $query, $results)
    {
        // 直接返回結果集,什麼都沒作
        return $results;
    }

後置處理器對select操做沒有作什麼後置操做,而是直接返回了。若是因爲業務須要作後置操做擴展的話,能夠在Extensions/文件夾下作override這個方法。再看下runSelect()的源碼:

protected function runSelect()
    {
        return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo);
    }
    
    public function getBindings()
    {
        // 把在where()方法存儲在$bindings[]中的值取出來
        return Arr::flatten($this->bindings);
    }

從上面源碼能猜出個大概邏輯:toSql()方法大概就是把API編譯成SQL語句,同時並把getBindings()中的真正的值取出來與SQL語句進行值綁定select()大概就是執行準備好的SQL語句。這個過程就像是先準備好$sql語句,而後就是常見的PDO->prepare($sql)->execute($bindings)。在這裏也可看到若是想知道DB::tables('users')->where('id', '=', 1)->get()被編譯後的SQL語句是啥,能夠這麼寫:DB::tables('users')->where('id', '=', 1)->toSql()

OK, toSqlselect()源碼在下篇再聊吧。

總結:本文主要學習了Query Builder的數據庫鏈接器和編譯API爲SQL相關源碼。編譯SQL細節和執行SQL的過程下篇再聊,到時見。

相關文章
相關標籤/搜索