经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » PHP » 查看文章
Doctrine\ORM\QueryBuilder 源码解析之 where
来源:cnblogs  作者:bytehello  时间:2020/11/16 10:29:24  对本文有异议

背景

最近有需求实现类似于 QueryBuilder 的谓词语句,就去翻看了它的源码。先看两个例子
例子1

  1. $qb = $em->createQueryBuilder();
  2. $qb->select('*')->from('User', 'u')->where('u.id = 1');
  3. echo $qb->getDQL();

例子2

  1. $qb = $em->createQueryBuilder();
  2. $qb->select('*')
  3. ->from('User', 'u')
  4. ->where('u.id = 1')
  5. ->andWhere('u.score >= 90')
  6. ->orWhere('u.score <= 100');
  7. echo $qb->getDQL();

是不是有点懵逼,希望看完我的文章,希望你有所收获。
接下来会以例子2讲解,分别解释 whereandWhereorWhere 方法,图文并茂,一步步教你理解上述PHP代码转换为sql语句的原理。

代码不难,建议大家配合源码食用。[相关源码在此]https://github.com/doctrine/orm/blob/master/lib/Doctrine/ORM/QueryBuilder.php(https://github.com/doctrine/orm/blob/master/lib/Doctrine/ORM/QueryBuilder.php
)

添加谓词语句

先看 QueryBuilder::where 方法,生成了一个Andx谓词对象 $predicates

  1. public function where($predicates)
  2. {
  3. if ( ! (func_num_args() == 1 && $predicates instanceof Expr\Composite)) {
  4. $predicates = new Expr\Andx(func_get_args());
  5. }
  6. return $this->add('where', $predicates);
  7. }

执行 var_export($predicates) 后查看成员变量如下,separator 是 where 子句条件之间的连接符,有 AND 和 OR。parts 是由条件组成的数组。

  1. // var_export($predicates)的输出
  2. Doctrine\ORM\Query\Expr\Andx::__set_state(array(
  3. 'separator' => ' AND ',
  4. 'allowedClasses' =>
  5. array (
  6. 0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
  7. 1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
  8. 2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
  9. 3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
  10. ),
  11. 'preSeparator' => '(',
  12. 'postSeparator' => ')',
  13. 'parts' =>
  14. array (
  15. 0 => 'u.id = 1',
  16. ),
  17. ))

接下来执行的return $this->add('where', $predicates);函数很简单,把谓词对象作为QueryBuilder::_dqlParts中的key为where的value,打印QueryBuilder对象如下

  1. // 此函数有删减,完整请看官方源码
  2. // QueryBuilder的add方法
  3. public function add($dqlPartName, $dqlPart, $append = false)
  4. {
  5. $isMultiple = is_array($this->_dqlParts[$dqlPartName])
  6. && !($dqlPartName == 'join' && !$append);
  7. // $isMultiple 这里的值为 false
  8. $this->_dqlParts[$dqlPartName] = ($isMultiple) ? [$dqlPart] : $dqlPart;
  9. $this->_state = self::STATE_DIRTY;
  10. return $this;
  11. }
  12. // _dqlParts 结构
  13. array (
  14. ...
  15. 'where' =>
  16. Doctrine\ORM\Query\Expr\Andx::__set_state(array(
  17. 'separator' => ' AND ',
  18. 'allowedClasses' =>
  19. array (
  20. 0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
  21. 1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
  22. 2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
  23. 3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
  24. ),
  25. 'preSeparator' => '(',
  26. 'postSeparator' => ')',
  27. 'parts' =>
  28. array (
  29. 0 => 'u.id = 1',
  30. ),
  31. ))
  32. ...

接下来具体看 QueryBuilder::andWhere方法,
getDQLPart取出的是刚才设置的Andx对象,接着执行Andx的addMultiple方法,最终调用的是Andx::add方法,这个方法最终是把'u.score >= 90'加入到Andx::parts数组中

  1. // QueryBuilder
  2. public function andWhere()
  3. {
  4. $args = func_get_args();
  5. $where = $this->getDQLPart('where');
  6. if ($where instanceof Expr\Andx) {
  7. $where->addMultiple($args);
  8. } else {
  9. array_unshift($args, $where);
  10. $where = new Expr\Andx($args);
  11. }
  12. return $this->add('where', $where);
  13. }
  14. // Andx
  15. public function add($arg)
  16. {
  17. if ( $arg !== null && (!$arg instanceof self || $arg->count() > 0) ) {
  18. // If we decide to keep Expr\Base instances, we can use this check
  19. if ( ! is_string($arg)) {
  20. $class = get_class($arg);
  21. if ( ! in_array($class, $this->allowedClasses)) {
  22. throw new \InvalidArgumentException("Expression of type '$class' not allowed in this context.");
  23. }
  24. }
  25. $this->parts[] = $arg;
  26. }
  27. return $this;
  28. }

所以此时Andx::parts数组中有了两个元素:'u.id = 1''u.score >= 90'

  1. Doctrine\ORM\Query\Expr\Andx::__set_state(array(
  2. 'separator' => ' AND ',
  3. 'allowedClasses' =>
  4. array (
  5. 0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
  6. 1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
  7. 2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
  8. 3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
  9. ),
  10. 'preSeparator' => '(',
  11. 'postSeparator' => ')',
  12. 'parts' =>
  13. array (
  14. 0 => 'u.id = 1',
  15. 1 => 'u.score >= 90',
  16. ),
  17. ))

继续看QueryBuilder::orWhere 方法,取出的 $where 是刚刚andWhere 执行后设置的 Andx 对象,执行 array_unshift($args, $where)语句后,形成的 $args 由一个 Andx 对象和一个字符串 'u.score <= 100'组成。

  1. public function orWhere()
  2. {
  3. $args = func_get_args();
  4. $where = $this->getDQLPart('where');
  5. if ($where instanceof Expr\Orx) {
  6. $where->addMultiple($args);
  7. } else {
  8. array_unshift($args, $where);
  9. $where = new Expr\Orx($args);
  10. }
  11. return $this->add('where', $where);
  12. }

将包含 Andx 对象和字符串 u.score <= 100$args 数组作为 Orx 对象的构造方法,Orx 构造函数的内部实现是 addMultiple方法,最终调用 Orx::add 方法将 $args 中的元素全部都加到 Orx对象 的 $parts 对象中,最终 Orx 对象的 parts 内容的示意图的阶段 3 所示

image.png
我整理了一下添加逻辑如下所示
image.png

解析谓词语句

谓词对象转换成谓词语句其实就是一句话,

  1. $queryPart = $this->getDQLPart($queryPartName);
  2. echo $queryPart;

不要觉得奇怪,对象也可以当作字符串用,引用PHP手册上的原话

__toString() 方法用于一个类被当成字符串时应怎样回应。

谓词对象的__toString的实现在Doctrine\ORM\Query\Expr\Composite,一起来看看

  1. public function __toString()
  2. {
  3. if ($this->count() === 1) {
  4. return (string) $this->parts[0];
  5. }
  6. $components = [];
  7. foreach ($this->parts as $part) {
  8. $components[] = $this->processQueryPart($part);
  9. }
  10. return implode($this->separator, $components);
  11. }
  12. private function processQueryPart($part)
  13. {
  14. $queryPart = (string) $part;
  15. if (is_object($part) && $part instanceof self && $part->count() > 1) {
  16. return $this->preSeparator . $queryPart . $this->postSeparator;
  17. }
  18. if (stripos($queryPart, ' OR ') !== false || stripos($queryPart, ' AND ') !== false) {
  19. return $this->preSeparator . $queryPart . $this->postSeparator;
  20. }
  21. return $queryPart;
  22. }

带入之前组合好的 Orx 对象,一起来分析下。Orx 对象parts属性的两个元素分别会被带入processQueryPart执行。
Andx 你先来,走到$queryPart = (string) $part,我们希望$part被当作字符串处理,继续回到__toString,这里是个递归。
Andx对象parts属性的两个字符串元素继续带入processQueryPart执行。这两个字符串经过处理会作为Andx对象的$components的元素,最终经过implode($this->separator, $components)返回字符串 u.id=1 and u.score >= 90 ,此时的值会被返回到 $queryPart

接下来执行到的是 return $this->preSeparator . $queryPart . $this->postSeparator; 到 Orx 对象的 $components的数组中去。

  1. // $part 是 Andx 对象
  2. // Andx 对象 经过字符串化后成了 u.id=1 and u.score >= 90,赋值给 $queryPart
  3. $queryPart = (string) $part;
  4. // 因为Andx对象有两个条件,所以左右两边会被加上括号,最终返回 (u.id = 1 AND u.score >= 90)
  5. if (is_object($part) && $part instanceof self && $part->count() > 1) {
  6. return $this->preSeparator . $queryPart . $this->postSeparator;
  7. }

Orx 对象parts属性的第一个元素已经处理完毕,接下来是第二个元素u.score <= 100,字符串就很简单了,直接返回到 Orx 对象的 $components 中去!
现在来看看Orx 对象的 $components中间有啥了

  1. array (
  2. 0 => '(u.id = 1 AND u.score >= 90)',
  3. 1 => 'u.score <= 100',
  4. )

再用implode切割成字符串 结果就是出来了(u.id = 1 AND u.score >= 90) OR u.score <= 100,解析完毕!看不懂的同学看我整理的流程图。
image.png

总结

具体细节大家可以使用 phpStorm + xdebug 单步调试研究。

原文链接:http://www.cnblogs.com/abyte/p/13969172.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号