Doctrine\ORM\QueryBuilder 源码解析之 where
背景
最近有需求实现类似于 QueryBuilder 的谓词语句,就去翻看了它的源码。先看两个例子
例子1
$qb = $em->createQueryBuilder();
$qb->select('*')->from('User', 'u')->where('u.id = 1');
echo $qb->getDQL();
例子2
$qb = $em->createQueryBuilder();
$qb->select('*')
->from('User', 'u')
->where('u.id = 1')
->andWhere('u.score >= 90')
->orWhere('u.score <= 100');
echo $qb->getDQL();
是不是有点懵逼,希望看完我的文章,希望你有所收获。
接下来会以例子2讲解,分别解释 where、andWhere、orWhere 方法,图文并茂,一步步教你理解上述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,
public function where($predicates)
{
if ( ! (func_num_args() == 1 && $predicates instanceof Expr\Composite)) {
$predicates = new Expr\Andx(func_get_args());
}
return $this->add('where', $predicates);
}
执行 var_export($predicates) 后查看成员变量如下,separator 是 where 子句条件之间的连接符,有 AND 和 OR。parts 是由条件组成的数组。
// var_export($predicates)的输出
Doctrine\ORM\Query\Expr\Andx::__set_state(array(
'separator' => ' AND ',
'allowedClasses' =>
array (
0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
),
'preSeparator' => '(',
'postSeparator' => ')',
'parts' =>
array (
0 => 'u.id = 1',
),
))
接下来执行的return $this->add('where', $predicates);函数很简单,把谓词对象作为QueryBuilder::_dqlParts中的key为where的value,打印QueryBuilder对象如下
// 此函数有删减,完整请看官方源码
// QueryBuilder的add方法
public function add($dqlPartName, $dqlPart, $append = false)
{
$isMultiple = is_array($this->_dqlParts[$dqlPartName])
&& !($dqlPartName == 'join' && !$append);
// $isMultiple 这里的值为 false
$this->_dqlParts[$dqlPartName] = ($isMultiple) ? [$dqlPart] : $dqlPart;
$this->_state = self::STATE_DIRTY;
return $this;
}
// _dqlParts 结构
array (
...
'where' =>
Doctrine\ORM\Query\Expr\Andx::__set_state(array(
'separator' => ' AND ',
'allowedClasses' =>
array (
0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
),
'preSeparator' => '(',
'postSeparator' => ')',
'parts' =>
array (
0 => 'u.id = 1',
),
))
...
接下来具体看 QueryBuilder::andWhere方法,
getDQLPart取出的是刚才设置的Andx对象,接着执行Andx的addMultiple方法,最终调用的是Andx::add方法,这个方法最终是把'u.score >= 90'加入到Andx::parts数组中
// QueryBuilder
public function andWhere()
{
$args = func_get_args();
$where = $this->getDQLPart('where');
if ($where instanceof Expr\Andx) {
$where->addMultiple($args);
} else {
array_unshift($args, $where);
$where = new Expr\Andx($args);
}
return $this->add('where', $where);
}
// Andx
public function add($arg)
{
if ( $arg !== null && (!$arg instanceof self || $arg->count() > 0) ) {
// If we decide to keep Expr\Base instances, we can use this check
if ( ! is_string($arg)) {
$class = get_class($arg);
if ( ! in_array($class, $this->allowedClasses)) {
throw new \InvalidArgumentException("Expression of type '$class' not allowed in this context.");
}
}
$this->parts[] = $arg;
}
return $this;
}
所以此时Andx::parts数组中有了两个元素:'u.id = 1' 和 'u.score >= 90'
Doctrine\ORM\Query\Expr\Andx::__set_state(array(
'separator' => ' AND ',
'allowedClasses' =>
array (
0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
),
'preSeparator' => '(',
'postSeparator' => ')',
'parts' =>
array (
0 => 'u.id = 1',
1 => 'u.score >= 90',
),
))
继续看QueryBuilder::orWhere 方法,取出的 $where 是刚刚andWhere 执行后设置的 Andx 对象,执行 array_unshift($args, $where)语句后,形成的 $args 由一个 Andx 对象和一个字符串 'u.score <= 100'组成。
public function orWhere()
{
$args = func_get_args();
$where = $this->getDQLPart('where');
if ($where instanceof Expr\Orx) {
$where->addMultiple($args);
} else {
array_unshift($args, $where);
$where = new Expr\Orx($args);
}
return $this->add('where', $where);
}
将包含 Andx 对象和字符串 u.score <= 100的 $args 数组作为 Orx 对象的构造方法,Orx 构造函数的内部实现是 addMultiple方法,最终调用 Orx::add 方法将 $args 中的元素全部都加到 Orx对象 的 $parts 对象中,最终 Orx 对象的 parts 内容的示意图的阶段 3 所示

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

解析谓词语句
谓词对象转换成谓词语句其实就是一句话,
$queryPart = $this->getDQLPart($queryPartName);
echo $queryPart;
不要觉得奇怪,对象也可以当作字符串用,引用PHP手册上的原话
__toString() 方法用于一个类被当成字符串时应怎样回应。
谓词对象的__toString的实现在Doctrine\ORM\Query\Expr\Composite,一起来看看
public function __toString()
{
if ($this->count() === 1) {
return (string) $this->parts[0];
}
$components = [];
foreach ($this->parts as $part) {
$components[] = $this->processQueryPart($part);
}
return implode($this->separator, $components);
}
private function processQueryPart($part)
{
$queryPart = (string) $part;
if (is_object($part) && $part instanceof self && $part->count() > 1) {
return $this->preSeparator . $queryPart . $this->postSeparator;
}
if (stripos($queryPart, ' OR ') !== false || stripos($queryPart, ' AND ') !== false) {
return $this->preSeparator . $queryPart . $this->postSeparator;
}
return $queryPart;
}
带入之前组合好的 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的数组中去。
// $part 是 Andx 对象
// Andx 对象 经过字符串化后成了 u.id=1 and u.score >= 90,赋值给 $queryPart
$queryPart = (string) $part;
// 因为Andx对象有两个条件,所以左右两边会被加上括号,最终返回 (u.id = 1 AND u.score >= 90)
if (is_object($part) && $part instanceof self && $part->count() > 1) {
return $this->preSeparator . $queryPart . $this->postSeparator;
}
Orx 对象parts属性的第一个元素已经处理完毕,接下来是第二个元素u.score <= 100,字符串就很简单了,直接返回到 Orx 对象的 $components 中去!
现在来看看Orx 对象的 $components中间有啥了
array (
0 => '(u.id = 1 AND u.score >= 90)',
1 => 'u.score <= 100',
)
再用implode切割成字符串 结果就是出来了(u.id = 1 AND u.score >= 90) OR u.score <= 100,解析完毕!看不懂的同学看我整理的流程图。

总结
具体细节大家可以使用 phpStorm + xdebug 单步调试研究。
Doctrine\ORM\QueryBuilder 源码解析之 where的更多相关文章
- Android 开源项目源码解析(第二期)
Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...
- Session获取不到的情况及解决办法(源码解析)
本博客是自己在学习和工作途中的积累与总结,仅供自己参考,也欢迎大家转载,转载时请注明出处,请尊重他人努力成果,谢谢. 1. 当有连个sessionFactory时,容易产生获取不到session的情况 ...
- flask源码解析之上下文为什么用栈
楔子 我在之前的文章<flask源码解析之上下文>中对flask上下文流程进行了详细的说明,但是在学习的过程中我一直在思考flask上下文中为什么要使用栈完成对请求上下文和应用上下文的入栈 ...
- 【转载】Xutils3源码解析
Github源码地址:https://github.com/wyouflf/xUtils3 原文地址 :http://www.codekk.com/blogs/detail/54cfab086c476 ...
- Android 数据库 ObjectBox 源码解析
一.ObjectBox 是什么? greenrobot 团队(现有 EventBus.greenDAO 等开源产品)推出的又一数据库开源产品,主打移动设备.支持跨平台,最大的优点是速度快.操作简洁,目 ...
- Mybatis源码解析,一步一步从浅入深(一):创建准备工程
Spring SpringMVC Mybatis(简称ssm)是一个很流行的java web框架,而Mybatis作为ORM 持久层框架,因其灵活简单,深受青睐.而且现在的招聘职位中都要求应试者熟悉M ...
- Mybaits 源码解析 (九)----- 全网最详细,没有之一:一级缓存和二级缓存源码分析
像Mybatis.Hibernate这样的ORM框架,封装了JDBC的大部分操作,极大的简化了我们对数据库的操作. 在实际项目中,我们发现在一个事务中查询同样的语句两次的时候,第二次没有进行数据库查询 ...
- SpringBoot 源码解析 (九)----- Spring Boot的核心能力 - 整合Mybatis
本篇我们在SpringBoot中整合Mybatis这个orm框架,毕竟分析一下其自动配置的源码,我们先来回顾一下以前Spring中是如何整合Mybatis的,大家可以看看我这篇文章Mybaits 源码 ...
- Laravel源码解析之model(代码)
本篇文章给大家带来的内容是关于Laravel源码解析之model(代码),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 前言 提前预祝猿人们国庆快乐,吃好.喝好.玩好,我会在电视上看 ...
随机推荐
- STM32之旅6——WWDG
WWDG是stm32f103的窗口看门狗,使用的时钟是APB1的时钟,在使用wwdg是被一个小问题困扰了很久--没有打开中断,无法喂狗,一直复位. 初始化完之后需要使能中断: __HAL_WWDG_E ...
- STM32F103C8T6驱动WS2812b灯条
STM32F103C8T6驱动WS2812b灯条 几天小朋友到别人家玩,看上了人家的金鱼,人家就给了她一条小金鱼,有了小金鱼,怕它没氧气挂掉,买了一个氧气泵,没有东西喂它也不行,又买了一包鱼料,又因为 ...
- 关于 ECMAScript、JavaScript、ES6、ECMAScript 2015
ECMAScript 是一种规范,而 JavaScript 是对规范的实现.ECMA 是标准化组织. 最早的 JavaScript 是由 Netscape 公司开发的,并提交给 ECMA 标准化组织, ...
- spring cloud 实现基于Nacos权重的负载均衡
package com.water.step.service.user.nacos; import com.alibaba.nacos.api.exception.NacosException; im ...
- 多测师讲解pthon_re模块_高级讲师肖sir
#import re 一.我们就re模块(也叫正则模块)介绍: 实现一个编译查找,一般在日志处理或者文件处理时用的比较多 正则表达式主要用于模式匹配和替换工作. 预定义字符集匹配: \d: ...
- C++冷知识(1)
func()等价于func(void) 也就是说在C++中,参数列表为空意味着不接受任何参数.之所以要注意这一点是因为在C语言中,参数列表为空意味着参数不确定.两者的语义是有巨大差别的,作为学了C再学 ...
- kali 運行 chrome
0x00前提 已經安裝好google chrome . 0x01 在終端執行命令: google-chrome,發現如圖: 錯誤提示:在root下只能使用 --no-sandbox選項來運行chrom ...
- nginx安全:配置网站图片防盗链
一,为什么要做防盗链? 1,什么是盗链? 比如某人有一个A网站, 他不愿自己存储图片,(因为磁盘和带宽都有成本) 就在自己A网站的页面上直接插入B网站的图片, 从而为自己吸引流量,这就是盗链 2,为什 ...
- Python初识和变量基础
Python是面向对象,动态解释型和强类型的语言 编译型: 将代码一次性全部编译成二进制,然后再执行 优点:执行效率高. 缺点:开发效率低. 代表语言:C 解释型: 逐行解释成二进制,逐行运行 优点: ...
- Baolu CSV Data Set Config
1.背景 大家在平常使用JMeter测试工具时,对CSV Data Set Config 配置元件肯定不会陌生.如果我们的压测场景涉及到数据库更新操作(如:转账接接口)则需要对参数化数据进行分块,可就 ...