背景

最近有需求实现类似于 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讲解,分别解释 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

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的更多相关文章

  1. Android 开源项目源码解析(第二期)

    Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...

  2. Session获取不到的情况及解决办法(源码解析)

    本博客是自己在学习和工作途中的积累与总结,仅供自己参考,也欢迎大家转载,转载时请注明出处,请尊重他人努力成果,谢谢. 1. 当有连个sessionFactory时,容易产生获取不到session的情况 ...

  3. flask源码解析之上下文为什么用栈

    楔子 我在之前的文章<flask源码解析之上下文>中对flask上下文流程进行了详细的说明,但是在学习的过程中我一直在思考flask上下文中为什么要使用栈完成对请求上下文和应用上下文的入栈 ...

  4. 【转载】Xutils3源码解析

    Github源码地址:https://github.com/wyouflf/xUtils3 原文地址 :http://www.codekk.com/blogs/detail/54cfab086c476 ...

  5. Android 数据库 ObjectBox 源码解析

    一.ObjectBox 是什么? greenrobot 团队(现有 EventBus.greenDAO 等开源产品)推出的又一数据库开源产品,主打移动设备.支持跨平台,最大的优点是速度快.操作简洁,目 ...

  6. Mybatis源码解析,一步一步从浅入深(一):创建准备工程

    Spring SpringMVC Mybatis(简称ssm)是一个很流行的java web框架,而Mybatis作为ORM 持久层框架,因其灵活简单,深受青睐.而且现在的招聘职位中都要求应试者熟悉M ...

  7. Mybaits 源码解析 (九)----- 全网最详细,没有之一:一级缓存和二级缓存源码分析

    像Mybatis.Hibernate这样的ORM框架,封装了JDBC的大部分操作,极大的简化了我们对数据库的操作. 在实际项目中,我们发现在一个事务中查询同样的语句两次的时候,第二次没有进行数据库查询 ...

  8. SpringBoot 源码解析 (九)----- Spring Boot的核心能力 - 整合Mybatis

    本篇我们在SpringBoot中整合Mybatis这个orm框架,毕竟分析一下其自动配置的源码,我们先来回顾一下以前Spring中是如何整合Mybatis的,大家可以看看我这篇文章Mybaits 源码 ...

  9. Laravel源码解析之model(代码)

    本篇文章给大家带来的内容是关于Laravel源码解析之model(代码),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 前言 提前预祝猿人们国庆快乐,吃好.喝好.玩好,我会在电视上看 ...

随机推荐

  1. 序列化的JavaScript

    下载 序列化的JavaScript序列化的JavaScript 将JavaScript序列化为包含正则表达式.日期和函数的JSON超集. 概述 这个包中的代码最初是作为表示状态的内部模块.为了扩展它的 ...

  2. 笔记本电脑为什么有时候不用按FN+F1~12也可以控制音量、亮度全部等等

    对于经常要使用F1~F12的用户就很烦,比如编写前端代码的时候想直接按F12检查代码就是不行. 如何取消快捷键? 问题原因: 1.电脑默认使用了快捷键. 2.电脑按了FN+ESC锁定,只限于戴尔的电脑 ...

  3. intellij idea如何解决javax.servlet.http不存在

    正确的解决方法是:对项目名右键,选中Open Mudule Settings--选择左侧的Modules,选择右边的Dependencies--然后点击右侧边栏的绿色"+"号,点击 ...

  4. hdfs的JAVA必会操作

    hdfs的必会操作 创建目录 //创建目录 public static void mkdir(String filePath) throws URISyntaxException, IOExcepti ...

  5. spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)

    一,为什么使用jwt? 1,什么是jwt? Json Web Token, 它是JSON风格的轻量级的授权和身份认证规范, 可以实现无状态.分布式的Web应用授权 2,jwt的官网: https:// ...

  6. go 继承

    package main import "fmt" type Animal struct { Color string } // 继承动物结构体 type Dog struct { ...

  7. js实现无缝连接轮播图(七)实现左侧按钮的功能

    <!-- 这个animate.js 必须写到 index.js的上面引入 --><script src="js/animate.js"></scrip ...

  8. hystrix线程池隔离的原理与验证

    引子 幸福很简单: 今天项目半年规划被通过,终于可以早点下班.先坐公交,全程开着灯,买了了几天的书竟然有时间看了.半小时后,公交到站,换乘大巴车.车还等着上人的功夫,有昏暗的灯光,可以继续看会儿书.过 ...

  9. Windows下显示已经连过的Wifi密码

    Windows下显示已经连过的Wifi密码   1. 打开cmd   2. netsh   3. wlan show profile 可以得到所有已经连过的wifi   4. wlan show pr ...

  10. c++ 获取当前时间周初凌晨时间戳(获取当前时间周一凌晨时间戳)

    UINT64 GetWeekBeginTime(){ time_t t; t = time(0); tm* t_tm = localtime(&t); t_tm->tm_hour = 0 ...