资讯专栏INFORMATION COLUMN

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

qpal / 2395人阅读

摘要:,看下源码返回很容易知道返回值是,然后将该值存储在变量中,这时。看下的源码去除掉字符后为返回从源码中可知道返回值为,这时。

说明:本文主要学习下Query Builder编译Fluent ApiSQL的细节和执行SQL的过程。实际上,上一篇聊到了IlluminateDatabaseQueryBuilder这个非常重要的类,这个类含有三个主要的武器:MySqlConnection, MySqlGrammar, MySqlProcessorMySqlConnection主要就是在执行SQL时做连接MySql数据库操作,MySqlProcessor主要就是用来对执行SQL后的数据集做后置处理操作,这两点已经在之前上篇聊过,那MySqlGrammar就是SQL语法编译器,用来编译Fluent ApiSQL。最后使用MySqlConnection::select($sql, $bindings)执行SQL。

开发环境:Laravel5.3 + PHP7

Builder::toSql()

看下toSql()的源码:

</>复制代码

  1. public function toSql()
  2. {
  3. // $this->grammar = new MySqlGrammar
  4. return $this->grammar->compileSelect($this);
  5. }
  6. public function compileSelect(Builder $query)
  7. {
  8. $sql = parent::compileSelect($query);
  9. // 从上一篇文章知道,$unions属性没有存储值,$wheres属性是有值的
  10. if ($query->unions) {
  11. $sql = "(".$sql.") ".$this->compileUnions($query);
  12. }
  13. return $sql;
  14. }

这里首先会调用IlluminateDatabaseQueryGrammarsGrammar::compileSelect(Builder $query),看下compileSelect(Builder $query)的源码:

</>复制代码

  1. public function compileSelect(Builder $query)
  2. {
  3. // $original = ["*"]
  4. $original = $query->columns;
  5. if (is_null($query->columns)) {
  6. $query->columns = ["*"];
  7. }
  8. $sql = trim($this->concatenate($this->compileComponents($query)));
  9. $query->columns = $original;
  10. // $sql = "select * from users where id = ?"
  11. return $sql;
  12. }
  13. protected $selectComponents = [
  14. "aggregate",
  15. "columns",
  16. "from",
  17. "joins",
  18. "wheres",
  19. "groups",
  20. "havings",
  21. "orders",
  22. "limit",
  23. "offset",
  24. "lock",
  25. ];
  26. protected function compileComponents(Builder $query)
  27. {
  28. $sql = [];
  29. foreach ($this->selectComponents as $component) {
  30. //
  31. if (! is_null($query->$component)) {
  32. $method = "compile".ucfirst($component);
  33. // 1. compileColumns($builder, ["*"]) -> "select " . $this->columnize(["*"])
  34. // 2. compileFrom($builder, "users"); -> "from ".$this->wrapTable("users")
  35. // 3. compileWheres($builder, [ 0 => ["type" => "basic", "column" => "id", "operator" => "=", "value" => 1, "boolean" => "and"], ])
  36. // $sql = ["columns" => "select *", "from" => "from users", "wheres" => "where id = ?"]
  37. $sql[$component] = $this->$method($query, $query->$component);
  38. }
  39. }
  40. return $sql;
  41. }

从上文源码中可知道,首先依次遍历片段集合:aggregate,columns,from,joins,wheres,groups,havings,orders,limit,offset,lock,查看属性有无存储值。在上文中知道,在片段$columns,from,wheres存有值为["*"], "users", [["type" => "basic", "column" => "id", "operator" => "=", "value" => 1, "boolean" => "and"]],然后通过拼接字符串调用方法compileColumns($builder, ["*"]), compileFrom($builder, "users"), compileWheres($builder, array),依次看下这些方法的源码:

</>复制代码

  1. protected function compileColumns(Builder $query, $columns)
  2. {
  3. if (! is_null($query->aggregate)) {
  4. return;
  5. }
  6. // $select = "select "
  7. $select = $query->distinct ? "select distinct " : "select ";
  8. return $select.$this->columnize($columns);
  9. }
  10. // Illuminate/Database/Grammar
  11. public function columnize(array $columns)
  12. {
  13. // 依次经过wrap()函数封装下
  14. return implode(", ", array_map([$this, "wrap"], $columns));
  15. }
  16. public function wrap($value, $prefixAlias = false)
  17. {
  18. if ($this->isExpression($value)) {
  19. return $this->getValue($value);
  20. }
  21. if (strpos(strtolower($value), " as ") !== false) {
  22. $segments = explode(" ", $value);
  23. if ($prefixAlias) {
  24. $segments[2] = $this->tablePrefix.$segments[2];
  25. }
  26. return $this->wrap($segments[0])." as ".$this->wrapValue($segments[2]);
  27. }
  28. $wrapped = [];
  29. $segments = explode(".", $value);
  30. // $segments = ["*"]
  31. foreach ($segments as $key => $segment) {
  32. if ($key == 0 && count($segments) > 1) {
  33. $wrapped[] = $this->wrapTable($segment);
  34. } else {
  35. // $wrapped = ["*"]
  36. $wrapped[] = $this->wrapValue($segment);
  37. }
  38. }
  39. return implode(".", $wrapped);
  40. }
  41. protected function wrapValue($value)
  42. {
  43. if ($value === "*") {
  44. return $value;
  45. }
  46. return """.str_replace(""", """", $value).""";
  47. }

通过源码很容易知道compileColumns($builder, ["*"])返回值select "*",然后将该值以key-value形式存储在$sql变量中,这时$sql = ["columns" => "select "*""]
OK,看下compileFrom($builder,"users")源码:

</>复制代码

  1. protected function compileFrom(Builder $query, $table)
  2. {
  3. return "from ".$this->wrapTable($table);
  4. }
  5. // Illuminate/Database/Grammar
  6. public function wrapTable($table)
  7. {
  8. if ($this->isExpression($table)) {
  9. return $this->getValue($table);
  10. }
  11. // 返回"users"
  12. return $this->wrap($this->tablePrefix.$table, true);
  13. }

很容易知道返回值是from "users",然后将该值存储在$sql变量中,这时$sql = ["columns" => "select "*"", "from" => "from "users""]。OK,看下compileWheres($builder, array)的源码:

</>复制代码

  1. protected function compileWheres(Builder $query)
  2. {
  3. $sql = [];
  4. if (is_null($query->wheres)) {
  5. return "";
  6. }
  7. foreach ($query->wheres as $where) {
  8. $method = "where{$where["type"]}"; // "whereBasic"
  9. // "and " . $this->whereBasic($builder, ["type" => "basic", "column" => "id", "operator" => "=", "value" => 1, "boolean" => "and"]
  10. // -> $sql = ["and id = ?", ];
  11. $sql[] = $where["boolean"]." ".$this->$method($query, $where);
  12. }
  13. if (count($sql) > 0) {
  14. $sql = implode(" ", $sql);
  15. // $conjunction = "where"
  16. $conjunction = $query instanceof JoinClause ? "on" : "where";
  17. // 去除掉"and"字符后为"where id = ?"
  18. return $conjunction." ".$this->removeLeadingBoolean($sql);
  19. }
  20. return "";
  21. }
  22. protected function whereBasic(Builder $query, $where)
  23. {
  24. // $value = "?"
  25. $value = $this->parameter($where["value"]);
  26. // 返回"id = ?"
  27. return $this->wrap($where["column"])." ".$where["operator"]." ".$value;
  28. }

从源码中可知道返回值为where id = ?,这时$sql = ["columns" => "select "*"", "from" => "from "users"", "wheres" => "where id = ?"]

OK, 最后通过concatenate()函数把$sql值拼接成字符串select "*" from "users" where id = ?

</>复制代码

  1. protected function concatenate($segments)
  2. {
  3. return implode(" ", array_filter($segments, function ($value) {
  4. return (string) $value !== "";
  5. }));
  6. }

也就是说,通过SQL语法编译器MySqlGrammartable("users")->where("id", "=", 1)编译成了SQL语句select * from users where id = ?

MySqlConnection::select()

上文聊到Builder::runSelect()调用了三个方法:MySqlConnection::select(), Builder::toSql(), Builder::getBindings(),其中Builder::toSql()通过SQL语法编译器已经编译得到了SQL语句,Builder::getBindings()获取存储在$bindings[ ]的值。最后看下MySqlConnection::select()是如何执行SQL语句的:

</>复制代码

  1. public function select($query, $bindings = [], $useReadPdo = true)
  2. {
  3. // Closure就是用来执行SQL,并把$query = "select * from users where id =?", $bindings = 1作为参数传递进去
  4. return $this->run($query, $bindings, function (Connection $me, $query, $bindings) use ($useReadPdo) {
  5. if ($me->pretending()) {
  6. return [];
  7. }
  8. // $statement = PDO::prepare("select * from users where id =?")
  9. /** @var PDOStatement $statement */
  10. $statement = $this->getPdoForSelect($useReadPdo)->prepare($query);
  11. $me->bindValues($statement, $me->prepareBindings($bindings));
  12. //PDO三步走: SQL编译prepare() => 值绑定bindValue() => SQL执行execute()
  13. // PDO通过这种方式防止SQL注入
  14. $statement->execute();
  15. $fetchMode = $me->getFetchMode();
  16. $fetchArgument = $me->getFetchArgument();
  17. $fetchConstructorArgument = $me->getFetchConstructorArgument();
  18. if ($fetchMode === PDO::FETCH_CLASS && ! isset($fetchArgument)) {
  19. $fetchArgument = "StdClass";
  20. $fetchConstructorArgument = null;
  21. }
  22. // PDOStatement::fetchAll(PDO::FETCH_OBJ);
  23. return isset($fetchArgument)
  24. ? $statement->fetchAll($fetchMode, $fetchArgument, $fetchConstructorArgument)
  25. : $statement->fetchAll($fetchMode);
  26. });
  27. }
  28. protected function run($query, $bindings, Closure $callback)
  29. {
  30. $this->reconnectIfMissingConnection();
  31. $start = microtime(true);
  32. try {
  33. // 执行闭包函数
  34. $result = $this->runQueryCallback($query, $bindings, $callback);
  35. } catch (QueryException $e) {
  36. if ($this->transactions >= 1) {
  37. throw $e;
  38. }
  39. $result = $this->tryAgainIfCausedByLostConnection(
  40. $e, $query, $bindings, $callback
  41. );
  42. }
  43. $time = $this->getElapsedTime($start);
  44. $this->logQuery($query, $bindings, $time);
  45. return $result;
  46. }
  47. protected function runQueryCallback($query, $bindings, Closure $callback)
  48. {
  49. try {
  50. // 执行闭包函数
  51. $result = $callback($this, $query, $bindings);
  52. }catch (Exception $e) {
  53. throw new QueryException(
  54. $query, $this->prepareBindings($bindings), $e
  55. );
  56. }
  57. return $result;
  58. }

通过源码知道主要是执行闭包来实现连接数据库和执行SQL操作,其中$statement = $this->getPdoForSelect($useReadPdo)->prepare($query)这句代码实现了数据库的连接操作SQL语句送入MySQL服务器进行语句编译。上文中提前聊了通过数据库连接器MySqlConnector::connect()连接数据库,这里知道实际上连接数据库是在这个时刻才触发的,Laravel5.0版本好像还没有这么写:

</>复制代码

  1. protected function getPdoForSelect($useReadPdo = true)
  2. {
  3. return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
  4. }
  5. public function getPdo()
  6. {
  7. if ($this->pdo instanceof Closure) {
  8. // 连接数据库,获得PDO实例
  9. return $this->pdo = call_user_func($this->pdo);
  10. }
  11. return $this->pdo;
  12. }

通过源码知道执行SQL操作很简单,就是常见的PDO操作:PDO三步走: SQL编译PDO::prepare() => 值绑定PDOStatement::bindValue() => SQL执行PDOStatement::execute()。所以这里可看出Query Builder是在PHP PDO的基础上实现的一层封装,使得用更加面向对象的Fluent API来操作数据库,而不需要写一行SQL语句。

OK, 总的来说,通过了解Query Builder的实现原理后,知道其并不复杂或神秘,只是一个对PDO更友好封装的包裹,Query Builder有几个重要的类或概念:连接类MySqlConnection及其为其服务的连接器MySqlConnector;Builder 类;SQL语法解析器MySqlGrammar;后置处理器MySqlProcessor

OK, illuminate/database package不仅提供了Query Builder,还提供了Eloquent ORM。那Eloquent ORM又是什么,与Query Builder是什么关系呢?既然有了Query Builder,为何还提供了Eloquent ORM呢?
实际上,Eloquent ORM又是对Query Builder的封装,这样可以实现更多好用且Query Builder所没有的功能,如Model Relationships;Accessor/Mutator;Scopes等等。以后再聊Eloquent ORM的实现原理吧。

总结:本文主要学习了Query Builder编译SQL细节和执行SQL逻辑。后续在分享下Eloquent ORM的实现原理,到时见。

RightCapital招聘Laravel DevOps

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/21998.html

相关文章

  • Laravel学习笔记Query Builder源码解析(上)

    摘要:说明本文主要学习模块的源码。这里,就已经得到了链接器实例了,该中还装着一个,下文在其使用时再聊下其具体连接逻辑。 说明:本文主要学习Laravel Database模块的Query Builder源码。实际上,Laravel通过Schema Builder来设计数据库,通过Query Builder来CURD数据库。Query Builder并不复杂或神秘,只是在PDO扩展的基础上又开...

    Steve_Wang_ 评论0 收藏0
  • Laravel 学习笔记 Query Builder 源码解析(中)

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

    zhou_you 评论0 收藏0
  • Laravel源码解析Model

    摘要:根据单一责任开发原则来讲,在的开发过程中每个表都应建立一个对外服务和调用。类似于这样解析的数据操作分两种它们除了有各自的特色外,基本的数据操作都是通过调用方法去完成整个。内并没有太多的代码,大多都是处理数据库链接。 showImg(https://segmentfault.com/img/bVbhjvY?w=600&h=296); 前言 提前预祝猿人们国庆快乐,吃好、喝好、玩好,我会在...

    CloudwiseAPM 评论0 收藏0
  • Laravel学习笔记Schema Builder 和 Migration System(上)

    摘要:看下两个方法的源码同样是使用了对象来添加命令和。 说明:本文主要学习Schema Builder和Migration System的使用及相关原理。传统上在设计database时需要写大量的SQL语句,但Laravel提供了Schema Builder这个神器使得在设计database时使用面向对象方法来做,不需要写一行SQL,并且还提供了另一个神器Migration System,可...

    nevermind 评论0 收藏0
  • Laravel学习笔记Container源码解析

    摘要:实际上的绑定主要有三种方式且只是一种的,这些已经在学习笔记之实例化源码解析聊过,其实现方法并不复杂。从以上源码发现的反射是个很好用的技术,这里给出个,看下能干些啥打印结果太长了,就不粘贴了。 说明:本文主要学习Laravel中Container的源码,主要学习Container的绑定和解析过程,和解析过程中的依赖解决。分享自己的研究心得,希望对别人有所帮助。实际上Container的绑...

    huayeluoliuhen 评论0 收藏0

发表评论

0条评论

qpal

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<