Laravel 源码阅读指南 -- Database 查询构建器

上文我们说到执行 DB::table('users')->get() 是由 Connection 对象执行 table 方法返回了一个 QueryBuilder 对象,QueryBuilder 提供了一个方便的接口来创建及运行数据库查询语句,开发者在开发时使用 QueryBuilder 不需要写一行 SQL 语句就能操作数据库了,使得书写的代码更加的面向对象,更加的优雅。

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 的构造方法中。

接下我们到 QueryBuilder 类文件 \Illuminate\Database\Query\Builder.php 里看看它里面的源码

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;
    }

}

QueryBuilder 构建 SQL 参数#

下面再来看看 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 方法里都做了什么。

//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 实例处理结果集,最后返回经过处理后的结果集。 我们接下来看下这两个流程。

Grammar 将构建的 SQL 参数编译成 SQL 语句#

我们接着从 toSql() 方法开始接着往下看 Grammar 类

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(默认 *)、fromwheres 属性,那么我们见先来看看这三个属性的编译器:

/**
 * 编译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 * '] 接下来看看 fromwheres 部分

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));
}

所以编译完 fromwheres 部分后 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
    );
}

Connection 执行 SQL 语句#

$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 实例来完成的。

通过梳理流程我们知道:

  1. Laravel 是在第一次执行 SQL 前去连接数据库的,之所以 $pdo 一开始是一个闭包因为闭包会保存创建闭包时的上下文里传递给闭包的变量,这样就能延迟加载,在用到连接数据库的时候再去执行这个闭包连上数据库。

  2. 在程序中判断 SQL 是否执行成功最准确的方法是通过捕获 QueryException 异常

Processor 后置处理结果集#

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 核心代码学习中,欢迎访问阅读,多多交流。

本作品采用《CC 协议》,转载必须注明作者和本文链接
公众号:网管叨 bi 叨 | Golang、Laravel、Docker、K8s 等学习经验分享
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 2

文章内容很棒~~写的很清晰

6年前 评论

写的很清晰,过程中有些细节可以说一说的,比如 select 如何防注入,其实就是调用了 pdo 的 bindValue。我觉得多一点重要的细节会更好

3年前 评论

未填写
文章
113
粉丝
368
喜欢
487
收藏
317
排名:34
访问:20.4 万
私信
所有博文
社区赞助商