ThinkPHP数据库查询之Db类深度解析

星图妙赏 2021-01-18 18:32:20
前言

在日常开发过程中模型的使用是非常之多的,但是在开发过程只知道如何使用,并不知道内在是如何实现的,模型是不管接口还是后台都会使用到的东西。

关于视图在前后台分离的大趋势下,框架存在视图大多数还是针对于后台开发的使用。

本文也是对框架解读快到最后阶段了,接下来咔咔将带领大家一起学习关于在框架中Db类的奥秘。

下图为咔咔提供的脑图可以根据这个脑图进行阅读文章。

一、Db操作类和其它类对应关系解刨

在学习模型之前一定要知道的就是DB这个类,这个类也是对数据库的操作。

在框架中存在这样一个配置文件,在这个配置文件里边会存在关于数据库配置的一系列信息。

在接下来的过程中咔咔也会简单的创建一个数据库来做演示。

同样在框架的核心层存在两个类,分别为Db类和Model类,这两个类就是接下来的解析对象。

在解析Db操作类和其它类对应关系解刨之前,我们先创建一个数据库作为演示使用。

首先先来看一下Db类的信息。

通过上图我们可以看到关于Db类的一部分信息,就是使用Db类的一些查询方法。

但是来到Db类的最后可以看到一个熟悉的方法__callStatic。

这个方法在一直读咔咔文章的读者应该已经很是熟悉了,这个方法在门面源码解析那一节中进行过深入的了解。

对于这个方法只需要记住的是在调用没有声明的静态方法时会进行调用。

至于call_user_func_array这个函数的使用可以理解为,这个方法是内置函数,可以直接调用函数运行,也就是可以直接运行方法。

在通过刚刚的查看Db类的注释信息时可以看到Db类是使用着Connection这个类,也就是连接数据库类。

进入到这个类里边简单的看一下构造函数即可,至于是怎么一个运行顺序会在下文进行讲解。

在框架中操作控制器有俩大场景,第一中为Db类操作,第二种就是Model操作。

其中Connection·为连接器,Query为查询器,Builder为sql生成器,exception为异常类。

知道了以上的几个信息,在接下来的理解过程中会有一定的帮助,在下一节中将会对Db类库场景分析。

二、Db类库场景分析

先从一个简单的案例进行解析,先来看一下数据库的数据。

然后来到控制器写一个简单的查询案例,在创建控制器之前先使用命令进行创建一个测试控制器。

在这个控制器进行简单的查询数据。

查询结果如下

在这个案例中,可以看到使用的是Db::query这种查询方式,接下来对于这种查询方式进行简单的剖析。

接着执行流程就会来到Db这个类,在这个类中可以看到关于当对象访问不存在的静态方法时,__callStatic()方法会被自动调用。

这个方法在之前门面的讲解中进行了深度讲解。

从上图可以看到当执行访问对象不存在的静态方法时会执行到call_user_func_array调用回调函数,并把一个数组参数作为回调函数的参数

接着代码就会执行到static::connect()这行代码,由于本类Db没有继承任何的类,所以对于static这个的使用就是调用本类。

如果当Db类继承了其它类那么就会有一定的区别,这个区别就是关于static关键字,给大家做的一点点冷门知识得补充,当一个类继承一个类时,在父类实用static关键字时,默认调用的子类的方法。

切换数据库连接

因为没有任何继承,所以会来到本类的connect这个方法。

在这个类里边首先会返回结果为数据库配置信息。

然后会从配置信息中获取到query这个索引,最终返回\think\db\Query这个字符串,这里一定要注意返回的是字符串,不是这个类的实例。

紧接着就会执行到第三步创建数据库连接对象实例,接下里将会对这一步进行解析。

紧接着会来到文件实际执行的为 new \think\db\Query,最终会返回执行查询 返回数据集,返回数据为返回 object(think\db\Query)

关于这个$this->connection是在本类的构造函数进行设置的。

先简单的看一下这个构造函数,在这个构造函数中直接就设置了connection这个属性的值,所以在上图中可以使用。

在这里执行完成之后就会将返回的值给从一开始就解析的这个调用未声明的静态方法会进行调用。

其中static::connect()就是最终返回的值static::connect() 返回 object(think\db\Query)。

所以接下来代码会执行到  thinkphp/library/think/db/Query.php 的 query方法

$sql 就是在Db::query()中传递的sql语句,并且执行查询 返回数据集

最后这段代码会执行think\db\connector\Mysql的query方法

接下来来到think\db\connector\Mysql的query方法

在这个方法中主要做了三件事情。

$this->initConnect 初始化数据库连接

$this->PDOStatement->execute(); 执行查询

return $this->getResult($pdo, $procedure); 返回结果集

解析$this->initConnect 初始化数据库连接

在这个方法中可以看到是进行了一次配置信息获取,首先需要明白这个配置信息是什么。

这个配置项是在配置文件database中配置的,根据注释提供的信息可以看到主要是关于主从服务器设置的。

一般情况下是不会在框架中配置主从信息的,这里就不去解析框架是如何实现数据库的主从配置了。

在这个判断中在进行了一次判断当前数据库连接的id,然后执行了连接数据库方法。

这个方法最终会返回object(PDO)#33的一个实例信息。

$this->PDOStatement->execute(); 执行查询

第二件事情做的就是执行查询,接下来我们来详细说明一下这个到底是如何执行的。

在返回pdo实例时,将这个实例赋值给了$this->PDOStatement这个属性,所以会去PDO类中进行执行。

在这里大家需要明白一件事情就是关于execute这个方法,用于执行返回多个结果集、多个更新计数或二者组合的语句。

第三件事情返回结果集

直到这里就是执行的最后一步就是返回结果集。

这里使用的方法都是查询底层,就在去解析了,在这里就会返回最终查询结果。

最终结果就会返回给这里__callStatic方法,并且返回给上层的$res变量。

直到这里关于使用Db查询的执行流程就解析完了, 但是框架给封装的方法不仅仅只有query,其它的查询方式可以按照咔咔的这个流程在进行简单的分析。

最后执行的都会是这一节的最后几个流程,只是前边执行会有一点点区别而已。

三、Db类库巧妙结合连接器、查询器、sql生成器使用

在上目录中咔咔使用了query作为案例演示,这个使用在框架中是不建议使用的,因为在维护的方面会有一定的难度。

本节案例将会使用框架常用的查询数据库方式进行查询。

在上图中可以看到使用了平时最常用的查询方式,接下来将会对这组案例进行详细分析。

同样代码会来到Db类的__callStatic这个方法,这个方法就是在调用没有声明的静态方法会进行执行的。

这个方法跟call方法是有区别的,call方法是调用不存在的方法会进行调用,一定要注意俩者的区别。

对于上图方法中static::connect()执行最后会返回 object(think\db\Query)这个对象,至于内部流程的执行可以参考第二目录的内容。

所以执行流程会来到thinkphp/library/think/db/Query.php这个类的table方法。

参数就是table中传递的数据库表名tp_test。

按照上图提供的代码会对传递过来的表名进行三次判断。

第一次判断是否为字符串

第二次判断是否存在 )

第三次判断是否存在 ,

根据传递过来的字符串以上三个判断均不成立,于是会执行到下面流程。

在table这个方法中可以看到最后的执行流程就是将传递过来的表名存放在属性options这个里边。

并且最后会将think\db\Query Object这个对象进行返回。

where方法解析

table方法分析完成后会紧接着执行where方法,同样还是在类thinkphp/library/think/db/Query.php

上图中在这个类中可以看到一个方法func_get_args,这个方法会返回一个包含函数参数列表的数组。

这个方法平时都是跟call_user_func_array同时使用,之前咔咔也使用这俩个方法进行过一次案例实验。

然后会使用函数array_shift删除数组中的第一个元素(red),并返回被删除元素的值。

下图第一个结果为func_get_args这个方法获取出来的数据,第二组结果为array_shift这个方法返回的结果。

俩组结果返回的值可以进行对比一下,可以更好的理解array_shift的使用场景。

紧接着会进行分析查询表达式,也就是方法parseWhereExp做的事情。

在这个方法中需要注意一个点就是关于传递过来的这俩个参数。

参数一为查询逻辑,参数二就是在使用案例时传入的参数。

在代码的第一行就需要我们来学习的一个知识点instanceof。

instanceof可以判断某个对象是否是某个类的实例,判断一个对象是否实现了某个接口。

关于这个的使用案例在文章ThinkPHP源码解析之控制器这一文中做了详细的说明。

根据学习instanceof的作用可以清晰的明白第一个判断不会进行执行。

在继续学习以下的执行流程,根据咔咔圈出来的框来进行对代码进行简单的解析。

根据上图首先会对查询逻辑的符号全部转为小写

然后在进行判断$field instanceof Where传递过来的参数是否为Where类的实例。

最后一个判断就是$field instanceof Expression跟上一步是判断同样的功能。

所以说代码最终的执行逻辑就是下图圈到的部分。

还记得在案例过程中给where传递的参数就是一个数组。

如果将参数改为where('t_id',1)则就会走is_string($field)的这个流程,这个流程就交给大家了,咔咔就不去解析。

这里咔咔还是使用数组作为参数进行解析,那么代码依然会执行本类的parseArrayWhereItems这个方法

在这个方法中先需要知道key会返回什么,从当前内部指针位置返回元素键名。

所以代码会去执行if语句的判断,根据上边的所有判断都不符合所以会执行这段代码$where[] = [$key, is_array($val) ? 'IN' : '=', $val];

这段代码会判断循环数组的value值是否为数组,如果为数组就是in,反之为=,由于value为1所以数组的第二个值为=。

那么最终where的值就是下图打印的数据。

由于where不为空,代码执行流程会执行到下图位置,最终在返回本类实例。

find()执行流程

接着代码会还是执行本类的find方法,查找单条记录。

由于find中是没有传递参数的,所以代码会执行到$this->parseOptions();分析表达式(可用于查询或者写入操作)

就目前写的案例而言,这段看似很长的代码大家好好看看都可以看明白,最终依然是返回当前的所有参数。

以下就是返回的所以结果

真正的查询数据是这块代码$result = $this->connection->find($this);,这段代码会执行到文件thinkphp/library/think/db/Connection.php

从这块代码可以看到当查询一条数据时框架默认给加上了limit为1,至于为什么这么加你就需要查看一下sql优化方面的知识了。

在这里就是关于sql语句的生成,代码自己好好看看就会明白,咔咔解析的只是执行流程和具体代码简单的了解一下即可。

至于具体实现流程咔咔在后期如果有机会会单独把每个方法进行深度解析,那时就是主要针对代码的解析。

最终返回结果如下

以上就是关于Db在结合连接器,查询器,生成器实现的数据库查询功能。

截止到这里关于Db的场景就分析到这里,接下来咔咔将会对Model进行简单的分析。

四、关于getLastSql的实现过程

还是之前的案例,我们来使用这个方法打印一下结果来看一下是什么。

看到上图就知道是框架最终给生成的SQL语句,那么接下来咔咔就会带大家一起来探讨一下,这个sql语句是如何生成的。

下图为本次演示的案例,也就是咔咔下图圈出来的地方。

从上图圈出来的地方进行代码追踪会到文件thinkphp/library/think/Db.php,并且会去执行本类的__callStatic方法,这个方法就不在进行解释了,在上文和之前也已经提到过多次了。

并且返回结果也不去做声明了,上文也提到了,这里只需要知道最终返回结果为返回 object(think\db\Query)

根据上图的返回结果可以知道最终回去调用object(think\db\Query)这个类的getLastSql这个方法

根据这个方法可以知道是获取最近一次查询的sql语句。

这里就会有点疑问了,关于属性connection到底是什么,这里在进行一次简单的简析。

关于这种属性的声明一般都会在本类的构造函数或者父类的构造函数中进行声明,这也是在阅读源码时的一个小窍门。

于是我们首先就需要来到本类的构造函数来看一眼。

通过上图可以得知,此处使用了依赖注入的方式,所以Connection就是一个对象,并且也是框架中所谓的连接器。

所以说这个Connection对象就是下图打印出来的。

根据上图得知使用的类文件应该就是think\db\connector\Mysql那么就会执行这个类里边的getLastSql方法。

但是来到这个类执行你会发现这个类里边根本是没有这个方法。

根据上图的继承关系,我们就知道这个方法是在thinkphp/library/think/db/Connection.php这个类文件里边。

下图即是这个方法执行过程,可以看到存在俩个参数,但是这俩个参数还是一头雾水根本不知道是什么。

根据代码追踪我们对上图所出现的俩个参数先进行简单的说明

$this->queryStr当前SQL指令

$this->bind 绑定参数

追踪$this->queryStr这个属性值

走到这里估计有点蒙了吧!对于这个值有点确定不了了,指定不是靠打印可以获取到结果的。

当然还有另一种办法就是进行debug来断点调试。

但是既然咔咔带大家看源码呢!就不会用上边的俩种方式,会直接从源码中找到蛛丝马迹。

根据咔咔上边给提供的案例,执行的最后一步就是find方法,这个方法也是在thinkphp/library/think/db/Connection.php这个类里边,寻找单条记录。

那么我们就在这个方法中进行一点点的寻找,这里咔咔已经给大家圈好了,就是下图咔咔圈其起来的地方。

根据上图咔咔给的代码注释,第一个参数就是生成的SQL语句,来继续追踪这个方法看一下,此时这个方法依然会在本类thinkphp/library/think/db/Connection.php这个文件中实现query方法。

在这个方法中一眼就可以看见对于这个queryStr属性的设置,是直接给这个属性赋值,那么也就是说这个属性的值就是上一个SQL语句生成的SQL语句。

所以说这个getLastSql获取的就是在这个语句之前执行的SQL语句,也只能获取出最近执行的那个SQL语句。

以上就是关于getLastSql的实现原理,在这里需要注意的就是关于SQL的生成,这块属实有点复杂。

五、总结

截止到这里关于数据库中Db类的操作场景分析以及关于结合连接器,查询器,生成器就到这里结束了。

这里咔咔主要就是使用了俩种案例来进行执行,第一种为原生案例,第二种为框架封装的案例。

使用了这俩种案例来对源码进行了深度解析,但是在文档还有很多的实现方法,其它的方法只需要根据咔咔给的提示然后一点点解析即可。

不需要对所有的方法都进行执行,不管任何方法走的都是上文分析的方法,也是很简单。

最后在演示了一下关于使用getLastSql来获取最后一次执行的SQL语句查询,这里的实现原理主要就是在Db类操作数据库时,不管是使用find方法还是select方法最终都会走向一个方法那就是query方法。

同样在这个方法中存在一个属性值queryStr,也就是在这个时候将SQL语句赋值进去的,然后在使用getLastSql这个方法使用queryStr和bind属性在对SQL进行拼接,最总返回SQL语句。

坚持学习、坚持写博、坚持分享是咔咔从业以来一直所秉持的信念。希望在偌大互联网中咔咔的文章能带给你一丝丝帮助。我是咔咔,下期见。

0 阅读:97