說明:本文主要學習下Query Builder編譯Fluent Api
爲SQL
的細節和執行SQL的過程。實際上,上一篇聊到了\Illuminate\Database\Query\Builder
這個很是重要的類,這個類含有三個主要的武器:MySqlConnection, MySqlGrammar, MySqlProcessor
。MySqlConnection
主要就是在執行SQL時作鏈接
MySql數據庫操做,MySqlProcessor
主要就是用來對執行SQL後的數據集作後置處理
操做,這兩點已經在以前上篇聊過,那MySqlGrammar
就是SQL語法編譯器,用來編譯Fluent Api
爲SQL
。最後使用MySqlConnection::select($sql, $bindings)
執行SQL。sql
開發環境:Laravel5.3 + PHP7
數據庫
看下toSql()
的源碼:服務器
public function toSql() { // $this->grammar = new MySqlGrammar return $this->grammar->compileSelect($this); } public function compileSelect(Builder $query) { $sql = parent::compileSelect($query); // 從上一篇文章知道,$unions屬性沒有存儲值,$wheres屬性是有值的 if ($query->unions) { $sql = '('.$sql.') '.$this->compileUnions($query); } return $sql; }
這裏首先會調用Illuminate\Database\Query\GrammarsGrammar::compileSelect(Builder $query)
,看下compileSelect(Builder $query)
的源碼:閉包
public function compileSelect(Builder $query) { // $original = ['*'] $original = $query->columns; if (is_null($query->columns)) { $query->columns = ['*']; } $sql = trim($this->concatenate($this->compileComponents($query))); $query->columns = $original; // $sql = 'select * from users where id = ?' return $sql; } protected $selectComponents = [ 'aggregate', 'columns', 'from', 'joins', 'wheres', 'groups', 'havings', 'orders', 'limit', 'offset', 'lock', ]; protected function compileComponents(Builder $query) { $sql = []; foreach ($this->selectComponents as $component) { // if (! is_null($query->$component)) { $method = 'compile'.ucfirst($component); // 1. compileColumns($builder, ['*']) -> 'select ' . $this->columnize(['*']) // 2. compileFrom($builder, 'users'); -> 'from '.$this->wrapTable('users') // 3. compileWheres($builder, [ 0 => ['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and'], ]) // $sql = ['columns' => 'select *', 'from' => 'from users', 'wheres' => 'where id = ?'] $sql[$component] = $this->$method($query, $query->$component); } } return $sql; }
從上文源碼中可知道,首先依次遍歷片斷集合:aggregate,columns,from,joins,wheres,groups,havings,orders,limit,offset,lock
,查看屬性有無存儲值。在上文中知道,在片斷$columns,from,wheres
存有值爲['*'], 'users', [['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and']]
,而後經過拼接字符串調用方法compileColumns($builder, ['*']), compileFrom($builder, 'users'), compileWheres($builder, array)
,依次看下這些方法的源碼:app
protected function compileColumns(Builder $query, $columns) { if (! is_null($query->aggregate)) { return; } // $select = 'select ' $select = $query->distinct ? 'select distinct ' : 'select '; return $select.$this->columnize($columns); } // Illuminate/Database/Grammar public function columnize(array $columns) { // 依次通過wrap()函數封裝下 return implode(', ', array_map([$this, 'wrap'], $columns)); } public function wrap($value, $prefixAlias = false) { if ($this->isExpression($value)) { return $this->getValue($value); } if (strpos(strtolower($value), ' as ') !== false) { $segments = explode(' ', $value); if ($prefixAlias) { $segments[2] = $this->tablePrefix.$segments[2]; } return $this->wrap($segments[0]).' as '.$this->wrapValue($segments[2]); } $wrapped = []; $segments = explode('.', $value); // $segments = ['*'] foreach ($segments as $key => $segment) { if ($key == 0 && count($segments) > 1) { $wrapped[] = $this->wrapTable($segment); } else { // $wrapped = ['*'] $wrapped[] = $this->wrapValue($segment); } } return implode('.', $wrapped); } protected function wrapValue($value) { if ($value === '*') { return $value; } return '"'.str_replace('"', '""', $value).'"'; }
經過源碼很容易知道compileColumns($builder, ['*'])
返回值select "*"
,而後將該值以key-value形式存儲在$sql變量
中,這時$sql = ['columns' => 'select "*"']
。
OK,看下compileFrom($builder,'users')
源碼:函數
protected function compileFrom(Builder $query, $table) { return 'from '.$this->wrapTable($table); } // Illuminate/Database/Grammar public function wrapTable($table) { if ($this->isExpression($table)) { return $this->getValue($table); } // 返回"users" return $this->wrap($this->tablePrefix.$table, true); }
很容易知道返回值是from "users"
,而後將該值存儲在$sql變量
中,這時$sql = ['columns' => 'select "*"', 'from' => 'from "users"']
。OK,看下compileWheres($builder, array)
的源碼:學習
protected function compileWheres(Builder $query) { $sql = []; if (is_null($query->wheres)) { return ''; } foreach ($query->wheres as $where) { $method = "where{$where['type']}"; // 'whereBasic' // 'and ' . $this->whereBasic($builder, ['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and'] // -> $sql = ['and id = ?', ]; $sql[] = $where['boolean'].' '.$this->$method($query, $where); } if (count($sql) > 0) { $sql = implode(' ', $sql); // $conjunction = 'where' $conjunction = $query instanceof JoinClause ? 'on' : 'where'; // 去除掉'and'字符後爲'where id = ?' return $conjunction.' '.$this->removeLeadingBoolean($sql); } return ''; } protected function whereBasic(Builder $query, $where) { // $value = '?' $value = $this->parameter($where['value']); // 返回'id = ?' return $this->wrap($where['column']).' '.$where['operator'].' '.$value; }
從源碼中可知道返回值爲where id = ?
,這時$sql = ['columns' => 'select "*"', 'from' => 'from "users"', 'wheres' => 'where id = ?']
。fetch
OK, 最後經過concatenate()
函數把$sql值
拼接成字符串select "*" from "users" where id = ?
:ui
protected function concatenate($segments) { return implode(' ', array_filter($segments, function ($value) { return (string) $value !== ''; })); }
也就是說,經過SQL語法編譯器MySqlGrammar
把table('users')->where('id', '=', 1)
編譯成了SQL語句select * from users where id = ?
。this
上文聊到Builder::runSelect()
調用了三個方法:MySqlConnection::select(), Builder::toSql(), Builder::getBindings()
,其中Builder::toSql()
經過SQL語法編譯器已經編譯獲得了SQL語句,Builder::getBindings()
獲取存儲在$bindings[ ]
的值。最後看下MySqlConnection::select()
是如何執行SQL語句的:
public function select($query, $bindings = [], $useReadPdo = true) { // Closure就是用來執行SQL,並把$query = 'select * from users where id =?', $bindings = 1做爲參數傳遞進去 return $this->run($query, $bindings, function (Connection $me, $query, $bindings) use ($useReadPdo) { if ($me->pretending()) { return []; } // $statement = PDO::prepare('select * from users where id =?') /** @var \PDOStatement $statement */ $statement = $this->getPdoForSelect($useReadPdo)->prepare($query); $me->bindValues($statement, $me->prepareBindings($bindings)); //PDO三步走: SQL編譯prepare() => 值綁定bindValue() => SQL執行execute() // PDO經過這種方式防止SQL注入 $statement->execute(); $fetchMode = $me->getFetchMode(); $fetchArgument = $me->getFetchArgument(); $fetchConstructorArgument = $me->getFetchConstructorArgument(); if ($fetchMode === PDO::FETCH_CLASS && ! isset($fetchArgument)) { $fetchArgument = 'StdClass'; $fetchConstructorArgument = null; } // PDOStatement::fetchAll(PDO::FETCH_OBJ); return isset($fetchArgument) ? $statement->fetchAll($fetchMode, $fetchArgument, $fetchConstructorArgument) : $statement->fetchAll($fetchMode); }); } protected function run($query, $bindings, Closure $callback) { $this->reconnectIfMissingConnection(); $start = microtime(true); try { // 執行閉包函數 $result = $this->runQueryCallback($query, $bindings, $callback); } catch (QueryException $e) { if ($this->transactions >= 1) { throw $e; } $result = $this->tryAgainIfCausedByLostConnection( $e, $query, $bindings, $callback ); } $time = $this->getElapsedTime($start); $this->logQuery($query, $bindings, $time); return $result; } protected function runQueryCallback($query, $bindings, Closure $callback) { try { // 執行閉包函數 $result = $callback($this, $query, $bindings); }catch (Exception $e) { throw new QueryException( $query, $this->prepareBindings($bindings), $e ); } return $result; }
經過源碼知道主要是執行閉包來實現鏈接數據庫和執行SQL操做,其中$statement = $this->getPdoForSelect($useReadPdo)->prepare($query)
這句代碼實現了數據庫的鏈接操做
和SQL語句送入MySQL服務器進行語句編譯
。上文中提早聊了經過數據庫鏈接器MySqlConnector::connect()
鏈接數據庫,這裏知道實際上鍊接數據庫是在這個時刻
才觸發的,Laravel5.0版本好像尚未這麼寫:
protected function getPdoForSelect($useReadPdo = true) { return $useReadPdo ? $this->getReadPdo() : $this->getPdo(); } public function getPdo() { if ($this->pdo instanceof Closure) { // 鏈接數據庫,得到PDO實例 return $this->pdo = call_user_func($this->pdo); } return $this->pdo; }
經過源碼知道執行SQL操做很簡單,就是常見的PDO操做:PDO三步走: SQL編譯PDO::prepare() => 值綁定PDOStatement::bindValue() => SQL執行PDOStatement::execute()
。因此這裏可看出Query Builder
是在PHP PDO
的基礎上實現的一層封裝,使得用更加面向對象的Fluent API來操做數據庫,而不須要寫一行SQL語句。
OK, 總的來講,經過了解Query Builder
的實現原理後,知道其並不複雜或神祕,只是一個對PDO更友好封裝的包裹,Query Builder
有幾個重要的類或概念:鏈接類MySqlConnection及其爲其服務的鏈接器MySqlConnector;Builder 類;SQL語法解析器MySqlGrammar;後置處理器MySqlProcessor
。
OK, illuminate/database package
不只提供了Query Builder
,還提供了Eloquent ORM
。那Eloquent ORM又是什麼,與Query Builder是什麼關係呢?既然有了Query Builder
,爲什麼還提供了Eloquent ORM
呢?
實際上,Eloquent ORM
又是對Query Builder
的封裝,這樣能夠實現更多好用且Query Builder
所沒有的功能,如Model Relationships;Accessor/Mutator;Scopes等等。
之後再聊Eloquent ORM
的實現原理吧。
總結:本文主要學習了Query Builder編譯SQL細節和執行SQL邏輯。後續在分享下Eloquent ORM的實現原理,到時見。