說明:本篇主要學習數據庫鏈接階段和編譯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 + PHP7
mysql
鏈接工廠類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
;Processor
。Grammar
將會把Query Builder的fluent api編譯成SQL
,PDO
編譯執行該SQL語句獲得結果集results
,Processor
將會處理該結果集results
。OK,那Query Builder是如何把書寫的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, toSql
和select()
源碼在下篇再聊吧。
總結:本文主要學習了Query Builder的數據庫鏈接器和編譯API爲SQL相關源碼。編譯SQL細節和執行SQL的過程下篇再聊,到時見。