前面的文章咱们介绍了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核心代码学习里,欢迎访问阅读。