文档章节

Laravel 5.3之 Query Builder 源码解析(中)

botkenni
 botkenni
发布于 2018/06/08 17:00
字数 3661
阅读 4
收藏 0

        $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)方法源码:

    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语句就能操作数据库了,使得书写的代码更加的面向对象,更加的优美。看下其源码:

    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;ProcessorGrammar将会把Query Builder的fluent api编译成SQLPDO编译执行该SQL语句得到结果集resultsProcessor将会处理该结果集results。OK,那Query Builder是如何把书写的api编译成SQL呢?

编译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()的源码:

    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()方法:

    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, toSqlselect()源码在下篇再聊吧。

总结:本文主要学习了Query Builder的数据库连接器和编译API为SQL相关源码。编译SQL细节和执行SQL的过程下篇再聊,到时见。

 

通过构造函数知道该MySqlConnection有了三件利器:PDO实例;Grammar SQL语法编译器实例;Processor SQL结果处理器实例。那PDO实例是如何得到的呢?再看下连接工厂类的createPdoResolver($config)方法源码:

    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语句就能操作数据库了,使得书写的代码更加的面向对象,更加的优美。看下其源码:

    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;ProcessorGrammar将会把Query Builder的fluent api编译成SQLPDO编译执行该SQL语句得到结果集resultsProcessor将会处理该结果集results。OK,那Query Builder是如何把书写的api编译成SQL呢?

编译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()的源码:

    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()方法:

    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, toSqlselect()源码在下篇再聊吧。

总结:本文主要学习了Query Builder的数据库连接器和编译API为SQL相关源码。编译SQL细节和执行SQL的过程下篇再聊,到时

© 著作权归作者所有

共有 人打赏支持
botkenni
粉丝 20
博文 409
码字总数 434882
作品 0
西城
程序员
私信 提问
Laravel 5.3之 Query Builder 源码解析(中)

说明:本篇主要学习数据库连接阶段和编译SQL语句部分相关源码。实际上,上篇已经聊到通过连接工厂类构造出了实例(假设驱动driver是mysql),在该MySqlConnection中主要有三件利器:;;,其中是...

botkenni
2016/12/05
11
0
Laravel 学习笔记5.3之 Query Builder 源码解析(下)

说明:本文主要学习下Query Builder编译为的细节和执行SQL的过程。实际上,上一篇聊到了这个非常重要的类,这个类含有三个主要的武器:。主要就是在执行SQL时做MySql数据库操作,主要就是用来...

botkenni
2016/12/05
22
0
Laravel5.3之Query Builder源码解析(上)

说明:本文主要学习Laravel Database模块的Query Builder源码。实际上,Laravel通过Schema Builder来设计数据库,通过Query Builder来CURD数据库。Query Builder并不复杂或神秘,只是在PDO扩...

botkenni
2016/12/05
18
0
laravel Scout包在elasticsearch中的应用

laravel Scout包在elasticsearch中的应用 laravel的Scout包是针对自身的Eloquent模型开发的基于驱动的全文检索引擎。意思就是我们可以像使用ORM一样使用检索功能。不管你用的是什么搜索引擎,...

王二狗子11
2018/01/07
0
0
Laravel 5 配置数据库主从读写分离和源码分析

一,配置过程: Laravel5读写分离配置比较简单,只需修改config/database.php,下面以MySQL数据库为例 内容如下 'mysql' => [ ] 设置完毕之后,Laravel5默认将select的语句让read指定的数据库...

李佳顺
2018/05/31
0
0

没有更多内容

加载失败,请刷新页面

加载更多

《蒙福人生》读后感作文2900字

《蒙福人生》读后感作文2900字: 我们的财富从何来?投胎、勤奋、智慧、机遇?我相信会有很多小伙伴会把上面的选项都否掉,或是一定要加一个前提即我们是基督徒当然是从神而来。如果我们确定...

原创小博客
23分钟前
0
0
使用xtrabackup备份数据库

使用xtrabackup备份数据库 提供一个参考文档:http://xtra.aminglinux.com mysqldump对于导出几个G的数据库或几个表,还是不错的,速度并不慢。一旦数据量达到几十上百G,无论是对原库的压力...

wzb88
30分钟前
0
0
Java 命令行工具使用

待更新 https://lotabout.me/2018/QQA-Understanding-jstat-gc-output/

lemos
56分钟前
2
0
Python自制微信机器人:群发消息、自动接收好友

运营公众号也有半年了,今年5月份开始的,之前一直用一款windows工具来运营自动接受好友请求、群发文章、自动回复等操作,但颇有不便。 举几个场景: 突然在外面看到一篇文章很好,临时写了一...

上海小胖
59分钟前
1
0
C# WPF上位机实现和下位机TCP通讯

下位机使用北京大华程控电源DH1766-1,上位机使用WPF。实现了电压电流实时采集,曲线显示。上午在公司调试成功,手头没有程控电源,使用TCP服务端模拟。昨天写的TCP服务端正好排上用场。 界面...

SEOwhywhy
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部