Laravel 学习笔记之 Query Builder 源码解析(中)

说明:本篇主要学习数据库连接阶段和编译SQL语句部分相关源码。实际上,上篇已经聊到 Query Builder 通过连接工厂类 Connecti

说明:本篇主要学习数据库连接阶段和编译SQL语句部分相关源码。实际上,上篇已经聊到 Query Builder

通过连接工厂类 ConnectionFactory

构造出了 MySqlConnection

实例(假设驱动driver是mysql),在该MySqlConnection中主要有三件利器: /Illuminate/Database/MysqlConnector

; /Illuminate/Database/Query/Grammars/Grammar

; /Illuminate/Database/Query/Processors/Processor

,其中 /Illuminate/Database/MysqlConnector

是在 ConnectionFactory

中构造出来的并通过MySqlConnection的构造参数注入的,上篇中重点谈到的通过 createPdoResolver($config)

获取到的闭包函数作为参数注入到该 MySqlConnection

,而 /Illuminate/Database/Query/Grammars/Grammar

/Illuminate/Database/Query/Processors/Processor

是在MySqlConnection构造函数中通过setter注入的。

开发环境:Laravel5.3 + PHP7

数据库连接器

连接工厂类 ConnectionFactory

中通过简单工厂方法实例化了 MySqlConnection

,看下该connection的构造函数:

public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])

{

// 该$pdo就是连接工厂类createPdoResolver($config)得到的闭包

$this->pdo = $pdo;

// $database就是config/database.php中设置的connections.mysql.database字段,默认为homestead

$this->database = $database;

$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

; Processor

Grammar

将会把Query Builder的fluent api编译成 SQL

PDO

编译执行该SQL语句得到结果集 results

Processor

将会处理该结果集 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, toSql

select()

源码在下篇再聊吧。

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

未登录用户
全部评论0
到底啦