前面的文章咱們介紹了Laravel Database的基礎組件,這篇文章詳細地介紹一下Database裏很是重要的基礎組件查詢構建器
文章裏我就直接用QueryBuilder來指代查詢構建器
了。php
上文咱們說到執行DB::table('users')->get()
是由Connection對象執行table方法返回了一個QueryBuilder對象,QueryBuilder提供了一個方便的接口來建立及運行數據庫查詢語句,開發者在開發時使用QueryBuilder不須要寫一行SQL語句就能操做數據庫了,使得書寫的代碼更加的面向對象,更加的優雅。git
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的構造方法中。github
接下咱們到QueryBuilder類文件\Illuminate\Database\Query\Builder.php
裏看看它裏面的源碼sql
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;
}
}
複製代碼
下面再來看看where方法裏都執行裏什麼, 爲了方便閱讀咱們假定執行條件where('name', '=', 'James')
數據庫
//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方法裏都作了什麼。bash
//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實例處理結果集,最後返回通過處理後的結果集。 咱們接下來看下這兩個流程。閉包
咱們接着從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
(默認*)、from
、wheres
屬性,那麼咱們見先來看看這三個屬性的編譯器:函數
/**
* 編譯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 * ']
接下來看看from
和wheres
部分
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));
}
複製代碼
因此編譯完from
和wheres
部分後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
);
}
複製代碼
$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實例來完成的。
經過梳理流程咱們知道:
Laravel是在第一次執行SQL前去鏈接數據庫的,之因此$pdo一開始是一個閉包由於閉包會保存建立閉包時的上下文裏傳遞給閉包的變量,這樣就能延遲加載,在用到鏈接數據庫的時候再去執行這個閉包連上數據庫。
在程序中判斷SQL是否執行成功最準確的方法是經過捕獲QueryException
異常
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核心代碼學習裏,歡迎訪問閱讀。