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

1 $this->tablePrefix = $tablePrefix; 2 3 $this->config = $config; 4 5 $this->useDefaultQueryGrammar(); 6 7 $this->useDefaultPostProcessor(); 8 } 9 10 public function useDefaultQueryGrammar() 11 { 12 $this->queryGrammar = $this->getDefaultQueryGrammar(); 13 } 14 15 protected function getDefaultQueryGrammar() 16 { 17 return new \Illuminate\Database\Query\Grammars\Grammar; 18 } 19 20 public function useDefaultPostProcessor() 21 { 22 $this->postProcessor = $this->getDefaultPostProcessor(); 23 } 24 25 protected function getDefaultPostProcessor() 26 { 27 return new \Illuminate\Database\Query\Processors\Processor; 28 }

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

1 protected function createPdoResolver(array $config) 2 { 3 return function () use ($config) { 4 // 等同于(new MySqlConnector)->connect($config) 5 return $this->createConnector($config)->connect($config); 6 }; 7 }

闭包里的代码这里还没有执行,是在后续执行SQL语句时调用Connection::select()执行的,之前的Laravel版本是没有封装在闭包里而是先执行了连接操作,Laravel5.3是封装在了闭包里等着执行SQL语句再连接操作,应该是为了提高效率。不过,这里先看下其连接操作的源码,假设是先执行了连接操作:

1 public function connect(array $config) 2 { 3 // database.php中没有配置'unix_socket',则调用getHostDsn(array $config)函数 4 // $dsn = 'mysql:host=127.0.0.1;port=21;dbname=homestead',假设database.php中是默认配置 5 $dsn = $this->getDsn($config); 6 7 // 如果配置了'options',假设没有配置 8 $options = $this->getOptions($config); 9 10 // 创建一个PDO实例 11 $connection = $this->createConnection($dsn, $config, $options); 12 13 // 相当于PDO::exec("use homestead;") 14 if (! empty($config['database'])) { 15 $connection->exec("use `{$config['database']}`;"); 16 } 17 18 $collation = $config['collation']; 19 20 // 相当于PDO::prepare("set names utf8 collate utf8_unicode_ci")->execute() 21 if (isset($config['charset'])) { 22 $charset = $config['charset']; 23 24 $names = "set names '{$charset}'". 25 (! is_null($collation) ? " collate '{$collation}'" : ''); 26 27 $connection->prepare($names)->execute(); 28 } 29 30 // 相当于PDO::prepare("set time_zone UTC+8") 31 if (isset($config['timezone'])) { 32 $connection->prepare( 33 'set time_zone="'.$config['timezone'].'"' 34 )->execute(); 35 } 36 37 // 假设'modes','strict'没有设置 38 $this->setModes($connection, $config); 39 40 return $connection; 41 } 42 43 protected function getHostDsn(array $config) 44 { 45 // 使用extract()函数来读取一个关联数组,如['host' => '127.0.0.1', 'database' => 'homestead'] 46 // 则 $host = '127.0.0.1', $database = 'homestead', 很巧妙的一个函数 47 extract($config, EXTR_SKIP); 48 49 return isset($port) 50 ? "mysql:host={$host};port={$port};dbname={$database}" 51 : "mysql:host={$host};dbname={$database}"; 52 }

通过构造函数知道最重要的一个方法是createConnection($dsn, $config, $options),该方法实例化了一个PDO,这里就明白了Query Builder也只是在PDO基础上封装的一层API集合,Query Builder提供的Fluent API使得不需要写一行SQL语句就能操作数据库了,使得书写的代码更加的面向对象,更加的优美。看下其源码:

1 public function createConnection($dsn, array $config, array $options) 2 { 3 $username = Arr::get($config, 'username'); 4 5 $password = Arr::get($config, 'password'); 6 7 try { 8 // 抓取出用户名和密码,直接new一个PDO实例 9 $pdo = $this->createPdoConnection($dsn, $username, $password, $options); 10 } catch (Exception $e) { 11 $pdo = $this->tryAgainIfCausedByLostConnection( 12 $e, $dsn, $username, $password, $options 13 ); 14 } 15 16 return $pdo; 17 } 18 19 protected function createPdoConnection($dsn, $username, $password, $options) 20 { 21 // 如果安装了Doctrine\DBAL\Driver\PDOConnection模块,就用这个类来实例化出一个PDO 22 if (class_exists(PDOConnection::class)) { 23 return new PDOConnection($dsn, $username, $password, $options); 24 } 25 26 return new PDO($dsn, $username, $password, $options); 27 }

总之,通过上面的代码拿到了MySqlConnection对象,并且该对象有三件利器:PDO;Grammar;Processor。Grammar将会把Query Builder的fluent api编译成SQL,PDO编译执行该SQL语句得到结果集results,Processor将会处理该结果集results。OK,那Query Builder是如何把书写的api编译成SQL呢?

编译API成SQL

还是以上篇说到的一行简单的fluent api为例:

1Route::get('/query_builder', function() { 2 // Query Builder 3 // (new MySqlConnection)->table('users')->where('id', '=', 1)->get(); 4 return DB::table('users')->where('id', '=', 1)->get(); 5});

这里已经拿到了MySqlConnection对象,看下其table()的源码:

1 public function table($table) 2 { 3 return $this->query()->from($table); 4 } 5 6 public function query() 7 { 8 return new \Illuminate\Database\Query\Builder( 9 $this, $this->getQueryGrammar(), $this->getPostProcessor() 10 ); 11 } 12 13 // SQL语法编译器 14 public function getQueryGrammar() 15 { 16 return $this->queryGrammar; 17 } 18 19 // 后置处理器 20 public function getPostProcessor() 21 { 22 return $this->postProcessor; 23 }

很容易知道Query Builder提供的fluent api都是在Builder这个类里,上篇也说过这是个非常重要的类。该Builder还必须装载两个神器:Grammar SQL语法编译器;Processor SQL结果集后置处理器。看下Builder类的from()方法:

1 public function from($table) 2 { 3 $this->from = $table; 4 5 return $this; 6 }

只是简单的赋值给$from属性,并返回Builder对象,这样就可以实现fluent api。OK,看下where('id', '=', 1)的源码:

1public function where($column, $operator = null, $value = null, $boolean = 'and') 2 { 3 // 从这里也可看出where()语句可以这样使用: 4 // where(['id' => 1]) 5 // where([ 6 // ['name', '=', 'laravel'], 7 // ['status', '=', 'active'], 8 // ]) 9 if (is_array($column)) { 10 return $this->addArrayOfWheres($column, $boolean); 11 } 12 13 // $value = 1, $operator = '=',这里可看出如果这么写where('id', 1)也可以 14 // 因为prepareValueAndOperator会把第二个参数1作为$value,并给$operator赋值'=' 15 list($value, $operator) = $this->prepareValueAndOperator( 16 $value, $operator, func_num_args() == 2 // func_num_args()为3,3个参数 17 ); 18 19 // where()也可以传闭包作为参数 20 if ($column instanceof Closure) { 21 return $this->whereNested($column, $boolean); 22 } 23 24 // 检查操作符是否非法 25 if (! in_array(strtolower($operator), $this->operators, true) && 26 ! in_array(strtolower($operator), $this->grammar->getOperators(), true)) { 27 list($value, $operator) = [$operator, '=']; 28 } 29 30 // 这里$value = 1,不是闭包 31 if ($value instanceof Closure) { 32 return $this->whereSub($column, $operator, $value, $boolean); 33 } 34 35 // where('name')相当于'name' = null作为过滤条件 36 if (is_null($value)) { 37 return $this->whereNull($column, $boolean, $operator != '='); 38 } 39 40 $type = 'Basic'; 41 42 // $column没有包含'->'字符 43 if (Str::contains($column, '->') && is_bool($value)) { 44 $value = new Expression($value ? 'true' : 'false'); 45 } 46 47 // $wheres = [ 48 // ['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and'], 49 // ]; 50 // 所以如果多个where语句如where('id', '=', 1)->where('status', '=', 'active'),则依次在$wheres中注册: 51 // $wheres = [ 52 // ['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and'], 53 // ['type' => 'basic', 'column' => 'status', 'operator' => '=', 'value' => 'active', 'boolean' => 'and'], 54 // ]; 55 $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); 56 57 if (! $value instanceof Expression) { 58 // 这里是把$value与'where'标记符绑定在该Builder的$bindings属性中 59 // 这时,$bindings = [ 60 // 'where' => [1], 61 // ]; 62 $this->addBinding($value, 'where'); 63 } 64 65 // 最后返回该Query Builder对象 66 return $this; 67 }

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

1 public function get($columns = ['*']) 2 { 3 $original = $this->columns; 4 5 if (is_null($original)) { 6 // $this->columns = ['*'] 7 $this->columns = $columns; 8 } 9 10 // processSelect()作为后置处理器处理query操作后的结果集 11 $results = $this->processor->processSelect($this, $this->runSelect()); 12 13 $this->columns = $original; 14 15 return collect($results); 16 }

从上面的源码可看出重点有两步:一是runSelect()编译执行SQL;二是后置处理器processor处理query操作后的结果集。说明runSelect()方法干了两件大事:编译API为SQL;执行SQL。在看下这两步骤之前,先看下后置处理器对查询的结果集做了什么后置操作:

1 // \Illuminate\Database\Query\Processors\Processor 2 public function processSelect(Builder $query, $results) 3 { 4 // 直接返回结果集,什么都没做 5 return $results; 6 }

后置处理器对select操作没有做什么后置操作,而是直接返回了。如果由于业务需要做后置操作扩展的话,可以在Extensions/文件夹下做override这个方法。再看下runSelect()的源码:

1 protected function runSelect() 2 { 3 return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo); 4 } 5 6 public function getBindings() 7 { 8 // 把在where()方法存储在$bindings[]中的值取出来 9 return Arr::flatten($this->bindings); 10 }

从上面源码能猜出个大概逻辑: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的过程下篇再聊,到时见。

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

1 protected function createPdoResolver(array $config) 2 { 3 return function () use ($config) { 4 // 等同于(new MySqlConnector)->connect($config) 5 return $this->createConnector($config)->connect($config); 6 }; 7 }

闭包里的代码这里还没有执行,是在后续执行SQL语句时调用Connection::select()执行的,之前的Laravel版本是没有封装在闭包里而是先执行了连接操作,Laravel5.3是封装在了闭包里等着执行SQL语句再连接操作,应该是为了提高效率。不过,这里先看下其连接操作的源码,假设是先执行了连接操作:

1 public function connect(array $config) 2 { 3 // database.php中没有配置'unix_socket',则调用getHostDsn(array $config)函数 4 // $dsn = 'mysql:host=127.0.0.1;port=21;dbname=homestead',假设database.php中是默认配置 5 $dsn = $this->getDsn($config); 6 7 // 如果配置了'options',假设没有配置 8 $options = $this->getOptions($config); 9 10 // 创建一个PDO实例 11 $connection = $this->createConnection($dsn, $config, $options); 12 13 // 相当于PDO::exec("use homestead;") 14 if (! empty($config['database'])) { 15 $connection->exec("use `{$config['database']}`;"); 16 } 17 18 $collation = $config['collation']; 19 20 // 相当于PDO::prepare("set names utf8 collate utf8_unicode_ci")->execute() 21 if (isset($config['charset'])) { 22 $charset = $config['charset']; 23 24 $names = "set names '{$charset}'". 25 (! is_null($collation) ? " collate '{$collation}'" : ''); 26 27 $connection->prepare($names)->execute(); 28 } 29 30 // 相当于PDO::prepare("set time_zone UTC+8") 31 if (isset($config['timezone'])) { 32 $connection->prepare( 33 'set time_zone="'.$config['timezone'].'"' 34 )->execute(); 35 } 36 37 // 假设'modes','strict'没有设置 38 $this->setModes($connection, $config); 39 40 return $connection; 41 } 42 43 protected function getHostDsn(array $config) 44 { 45 // 使用extract()函数来读取一个关联数组,如['host' => '127.0.0.1', 'database' => 'homestead'] 46 // 则 $host = '127.0.0.1', $database = 'homestead', 很巧妙的一个函数 47 extract($config, EXTR_SKIP); 48 49 return isset($port) 50 ? "mysql:host={$host};port={$port};dbname={$database}" 51 : "mysql:host={$host};dbname={$database}"; 52 }

通过构造函数知道最重要的一个方法是createConnection($dsn, $config, $options),该方法实例化了一个PDO,这里就明白了Query Builder也只是在PDO基础上封装的一层API集合,Query Builder提供的Fluent API使得不需要写一行SQL语句就能操作数据库了,使得书写的代码更加的面向对象,更加的优美。看下其源码:

1 public function createConnection($dsn, array $config, array $options) 2 { 3 $username = Arr::get($config, 'username'); 4 5 $password = Arr::get($config, 'password'); 6 7 try { 8 // 抓取出用户名和密码,直接new一个PDO实例 9 $pdo = $this->createPdoConnection($dsn, $username, $password, $options); 10 } catch (Exception $e) { 11 $pdo = $this->tryAgainIfCausedByLostConnection( 12 $e, $dsn, $username, $password, $options 13 ); 14 } 15 16 return $pdo; 17 } 18 19 protected function createPdoConnection($dsn, $username, $password, $options) 20 { 21 // 如果安装了Doctrine\DBAL\Driver\PDOConnection模块,就用这个类来实例化出一个PDO 22 if (class_exists(PDOConnection::class)) { 23 return new PDOConnection($dsn, $username, $password, $options); 24 } 25 26 return new PDO($dsn, $username, $password, $options); 27 }

总之,通过上面的代码拿到了MySqlConnection对象,并且该对象有三件利器:PDO;Grammar;Processor。Grammar将会把Query Builder的fluent api编译成SQL,PDO编译执行该SQL语句得到结果集results,Processor将会处理该结果集results。OK,那Query Builder是如何把书写的api编译成SQL呢?

编译API成SQL

还是以上篇说到的一行简单的fluent api为例:

1Route::get('/query_builder', function() { 2 // Query Builder 3 // (new MySqlConnection)->table('users')->where('id', '=', 1)->get(); 4 return DB::table('users')->where('id', '=', 1)->get(); 5});

这里已经拿到了MySqlConnection对象,看下其table()的源码:

1 public function table($table) 2 { 3 return $this->query()->from($table); 4 } 5 6 public function query() 7 { 8 return new \Illuminate\Database\Query\Builder( 9 $this, $this->getQueryGrammar(), $this->getPostProcessor() 10 ); 11 } 12 13 // SQL语法编译器 14 public function getQueryGrammar() 15 { 16 return $this->queryGrammar; 17 } 18 19 // 后置处理器 20 public function getPostProcessor() 21 { 22 return $this->postProcessor; 23 }

很容易知道Query Builder提供的fluent api都是在Builder这个类里,上篇也说过这是个非常重要的类。该Builder还必须装载两个神器:Grammar SQL语法编译器;Processor SQL结果集后置处理器。看下Builder类的from()方法:

1 public function from($table) 2 { 3 $this->from = $table; 4 5 return $this; 6 }

只是简单的赋值给$from属性,并返回Builder对象,这样就可以实现fluent api。OK,看下where('id', '=', 1)的源码:

1public function where($column, $operator = null, $value = null, $boolean = 'and') 2 { 3 // 从这里也可看出where()语句可以这样使用: 4 // where(['id' => 1]) 5 // where([ 6 // ['name', '=', 'laravel'], 7 // ['status', '=', 'active'], 8 // ]) 9 if (is_array($column)) { 10 return $this->addArrayOfWheres($column, $boolean); 11 } 12 13 // $value = 1, $operator = '=',这里可看出如果这么写where('id', 1)也可以 14 // 因为prepareValueAndOperator会把第二个参数1作为$value,并给$operator赋值'=' 15 list($value, $operator) = $this->prepareValueAndOperator( 16 $value, $operator, func_num_args() == 2 // func_num_args()为3,3个参数 17 ); 18 19 // where()也可以传闭包作为参数 20 if ($column instanceof Closure) { 21 return $this->whereNested($column, $boolean); 22 } 23 24 // 检查操作符是否非法 25 if (! in_array(strtolower($operator), $this->operators, true) && 26 ! in_array(strtolower($operator), $this->grammar->getOperators(), true)) { 27 list($value, $operator) = [$operator, '=']; 28 } 29 30 // 这里$value = 1,不是闭包 31 if ($value instanceof Closure) { 32 return $this->whereSub($column, $operator, $value, $boolean); 33 } 34 35 // where('name')相当于'name' = null作为过滤条件 36 if (is_null($value)) { 37 return $this->whereNull($column, $boolean, $operator != '='); 38 } 39 40 $type = 'Basic'; 41 42 // $column没有包含'->'字符 43 if (Str::contains($column, '->') && is_bool($value)) { 44 $value = new Expression($value ? 'true' : 'false'); 45 } 46 47 // $wheres = [ 48 // ['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and'], 49 // ]; 50 // 所以如果多个where语句如where('id', '=', 1)->where('status', '=', 'active'),则依次在$wheres中注册: 51 // $wheres = [ 52 // ['type' => 'basic', 'column' => 'id', 'operator' => '=', 'value' => 1, 'boolean' => 'and'], 53 // ['type' => 'basic', 'column' => 'status', 'operator' => '=', 'value' => 'active', 'boolean' => 'and'], 54 // ]; 55 $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); 56 57 if (! $value instanceof Expression) { 58 // 这里是把$value与'where'标记符绑定在该Builder的$bindings属性中 59 // 这时,$bindings = [ 60 // 'where' => [1], 61 // ]; 62 $this->addBinding($value, 'where'); 63 } 64 65 // 最后返回该Query Builder对象 66 return $this; 67 }

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

1 public function get($columns = ['*']) 2 { 3 $original = $this->columns; 4 5 if (is_null($original)) { 6 // $this->columns = ['*'] 7 $this->columns = $columns; 8 } 9 10 // processSelect()作为后置处理器处理query操作后的结果集 11 $results = $this->processor->processSelect($this, $this->runSelect()); 12 13 $this->columns = $original; 14 15 return collect($results); 16 }

从上面的源码可看出重点有两步:一是runSelect()编译执行SQL;二是后置处理器processor处理query操作后的结果集。说明runSelect()方法干了两件大事:编译API为SQL;执行SQL。在看下这两步骤之前,先看下后置处理器对查询的结果集做了什么后置操作:

1 // \Illuminate\Database\Query\Processors\Processor 2 public function processSelect(Builder $query, $results) 3 { 4 // 直接返回结果集,什么都没做 5 return $results; 6 }

后置处理器对select操作没有做什么后置操作,而是直接返回了。如果由于业务需要做后置操作扩展的话,可以在Extensions/文件夹下做override这个方法。再看下runSelect()的源码:

1 protected function runSelect() 2 { 3 return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo); 4 } 5 6 public function getBindings() 7 { 8 // 把在where()方法存储在$bindings[]中的值取出来 9 return Arr::flatten($this->bindings); 10 }

从上面源码能猜出个大概逻辑: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的过程下篇再聊,到时

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )