调试特点
如果 B 继承了 A, 那么在调试器中,只能看到 B 的直接属性或者方法,看不到它所继承的。先明白这一点
Manager
注意每一窗口下方的代码位置,方便快速定位代码。
【1】objects 是在 Django 环境加载的时候就已经添加了的,代码加载的时候会触发【2】
【2】当前的 Manager 类的实例是在代码加载的时候生成的,每一个自定义类都将自身这个类名写进了 Manager 实例中,也就是代码【2】
【3】Manager 并不是直接继承了 BaseManager 类,而是调用了 BaseManager.from_queryset
【4】动态生成了新的类,这个类名会有一个默认值,这个默认值需要计算一下,也可以直接指定类名
【5】获取 QuerySet 的所有的方法
【6】前面两种都被排除了,剩下的才会写进新类中,但是要注意的是,新类并没有直接得到 QuerySet 的符合条件的方法,而是采用了闭包加反射的手段。所以最后返回的一个 new_methods, 其实所有的键对应的值都长得差不多,不过这里对闭包函数进行了重命名。使用的就是 manager_method.__name__ = method.__name__
这一行代码。
注意:
objects 这个对象在代码加载的时候就生成了,但是他还是 BaseManager 的子类实例,他有 QuerySet 的同名方法,但是这些方法并不是 QuerySet 的方法,而是通过闭包加反射的手段做了个同名触发而已。但是需要注意的是,一旦 objects 调用了任何跟 QuerySet 同名的方法,都将会生成一个 QuerySet 的对象,所以之后的所有操作再跟 Manager 无关了。
QuerySet
这里不探究整个 QuerySet, 我们先看一个简单的 QuerySet 的执行流程。执行下面这段代码。
我们知道,QuerySet 可以无限 filter 等操作,但是只要不尝试获取值,他就不会查询数据库,这里我将这小段代码写进了 views 文件中,只要 Django 系统加载起来,这两行代码就会执行,他的打印值显示他查询过数据库,那么,我们尝试一下探寻一下这两行代码的内部源码,了解一下 QuerySet 获取数据库值的一个完整流程,以及其中出现的各个部件。
第一行有 filter 的代码并不会执行查询操作,真正触发了查询操作的是 print 函数。print 函数内部其实调用的是对象的 __repr__
函数。那么 __repr__
函数中一定触发过查询数据库的操作,正好可以从这里开始调试。
__repr__
函数
【1】print 函数触发了 __repr__
函数的执行
【2】这里尝试执行 list 函数,说明在执行 list 的时候就获取了数据库中的结果。同时我们还发现了一个比较有意思的代码,__repr__
函数对打印结果做出了限制,如果超过了 20 个就会截断了并显示了一串英文,我们在使用 Django 的时候经常遇到这种现象,而这个就是原因。
【3】这里需要做出解释,当前调试定位到了【2】,说明是准备执行【2】,而不是已经执行完了,那为什么调试器这里已经显示出了当前的 queryset 的实例已经存在值了。这是因为,调试器是尝试打印当前即将执行代码时,这段代码这里所能看到的所有对象的值,会隐式执行 print(obj)。也就是说调试光标停在了【2】这里,是因为【1】这行代码的执行触发了,而此时调试器尝试显示当前的 queryset 实例,于是又执行了一遍 print(queryset)
。请看下面这个例子
此处的 add_one 类似于 filter,会返回一个新的 A 的实例。调试器输出的每一个 A 实例中的 class_index。这说明,a3 这个变量先被打印,此刻断点定位,调试器尝试显示 a2 和 a1。解释器执行代码是从上而下执行的,所以三个 A 实例的 object_index 分别是 1,2,3 但是解释器想显示每一个变量的值,却是从下向上对当前环境变量挨个儿执行了一遍 print(obj),也就是说 __repr__
的调用顺序反而是 a3,a2,a1。于是就有了 class_index 和 object_index 的值正好相反的情况。我们猜测调试器显示得数据是调试器自己的打印行为,那么执行完断点代码之后,class_index 应该就是 4。
符合猜测。
那么只能说明调试器想显示当前的变量值,会调用当前位置能看到的所有的对象的 __repr__
方法,这个行为是不受断点影响的。而在这里,是因为先有了 print(qs2) 这段代码触发的断点行为,导致当前 queryset 实例被调试器打印了值,那么这个对我们调试有什么影响了,影响太大了。
看这里,因为浏览器尝试显示每一个当前的 object 对象,提前偷偷地查询了当前的 queryset 对应的数据,所以理论上来说我们明明是第一次走的【2】,接下来应该走【3】之后的代码。结果竟然是从缓存中取的数据,而不是准备走查询数据库的操作,为了搞明白这个原因,花费了我一天的时间。
所以为了避免缓存导致的这种情况,我们应该改写 QuerySet,当然不是改动原代码文件,而是拷贝一份,再改写一下。
这里有两个 views 文件,其中 views2 文件中已经拷贝了 QuerySet, 叫做 QuerySet2, 接下来我们会使用 QuerySet2 演示 QuerySet 的相关用法。接下来我们需要改写 __repr__
避免调试器提前调用了该方法,导致 QuertSet 的查询结果被缓存,观察不到完整的流程。
Model
class Province(models.Model):
"""
省份表
"""
name = models.CharField("省份", max_length=50)
class Meta:
db_table = 'province'
class City(models.Model):
"""
省份表
"""
province = models.ForeignKey(Province, on_delete=models.SET_NULL, verbose_name="省份", null=True)
name = models.CharField("城市", max_length=50)
class Meta:
db_table = 'city'
示例代码
qs1 = QuerySet2(model=City).using('icgoo_log').filter(province__id=20)
qs2 = qs1.values('province__name', 'name')
print(10)
一个意外
断点打在了 print(10) 并且 __repr__
已经被重写了,这时我们发现一个严重的问题
断点确实被触发了,但是这时调试器中依然能显示 qs1 中的值,但是按照我们学习的 Django 的知识,当我们改写了 __repr__
的时候,qs1 的缓存应该为空,这是为什么?
猜想:调试器会将对象的有些魔法方法执行一遍。
猜想正确
浏览 QuerySet 源码,感觉调试器中仍然出现了数据库中的值,极有可能是因为有哪个魔法方法调用了 _fetch_all 方法,而调试器极有可能会不受控制得触发这个魔法方法。搜索 QuerySet2 源码,发现一共有好几个魔法方法都调用了 _fetch_all 方法,我们可以使用装饰器暴力排查。
可以发现调试器确实调用了 __len__
和 __repr__
而这两个魔法方法内部原本都有 显式[__len__
] 或者 隐式[__repr__
] 得调用过 _fetch_all 方法。
其实内部还有一些细节,不过我没空去深究。深究了也容易忘记,只需要记住调试器会主动调用对象的 __len__
和 __str__
[__repr__
经过我打的相关实践发现他优先级低于 __str__
,如果当前类重写了 __str__
,就算 __repr__
被重写依然不会被调用]
我再次改写了一下装饰器,并且修改了 QuerySet2 给其克隆方法添加了自增功能,这样就知道本次测试中一共有几个 QuerySet 的实例,同时调试器只会尝试打印哪些。至于打印结果的顺序,我还不能思考明白,暂时不用理会这些。
改写了 __len__
方法
经过验证,确实去掉了调试器自己查询数据库写入缓存的行为,同时也证明了 values 方法依然不会触发数据库的查询。所以我们还需要继续改造一下代码,那么现在就剩下一个触发数据库查询行为的方法了,那就是切片。
切片
不过切片方法不能直接用,需要做出一点修改,主要是做了下面这些的打印行为。
改造完成。终于要开始撸代码了。
【1】是测试代码
【2】是断点调试初次挺住的地方
【3】在整个方法作用域内,遇到函数不跳进去,就这样一行一行执行代码
【4】不断执行【3】最终需要断点停在这里
【5】表名当前是第 5 个 QuerySet 的实例,符合代码分析,确实一共调用了 4 次 orm 的方法,加上初始化,一共生成了 5 个实例对象
【6】表名当前最新的一个实例还没有读取数据库,因为缓存数据。
当前我们不需要知道怎么实现的,只需要知道两个方法的作用,后面再做详细解释。_chain 克隆一个新的 QuerySet 的实例,_fetch_all 读取数据库生成一个个 Model 对象。
继续执行下一行代码。
调试发现,只有在即将返回的时候,缓存中才有值,而我们修改了切片操作,让其返回之前打印了每一个 Model 对象,调试器控制台也打印了每一个 Model 对象,而在打印之前,还执行了一下 __iter__
方法,因此去看一下 __iter__
方法。
__iter__
调用 _fetch_all 获取数据,并将结果封装为一个可以迭代的对象返回,方便 for 循环使用
_fetch_all
打上这个断点,重新调试,可以发现 _iterable_class 为 ValuesIterable。
QuerySet 的默认迭代类是 ModelIterable,但是被 values 方法改写为 ValuesIterable 类。既然我们这里使用的是 ValuesIterable, 所以去看一下 ValuesIterable
ValuesIterable
【1】ValuesIterable 的实例也是一个 可迭代对象
【2】获取一个编译对象,可以将一个 Query 对象编译成 Sql,然后查询数据库
【3】这个编译对象属于什么类
【4】获取所有的查询参数,也就是最终的 values 方法里的参数
【5】迭代对象生成器,他会不断吐出数据,同时这里以一个字典返回每一个单元数据
【6】一看就知道是查询了数据库并且返回。
SQLCompiler
还需要看他的父类
所以重点还是看看 execute_sql 方法
【1】返回的数据类型,无数据我怀疑是修改操作,这个后面在讨论
【2】获取 sql
【3】依据条件生成对应的 cursor,或为普通的 cursor,或为块 cursor,也就是分块获取 cursor
【4】执行了 sql
【5】或为从游标中获取数据
【6】返回数据
所以现在重点是【2】和【5】
其实到这里,整个大致流程已经摸清楚了。下一篇我们需要好好看一下 as_sql 是如何生成 sql 语句的。
标签:__,08,QuerySet,repr,objects,方法,代码,调试器 From: https://www.cnblogs.com/yaowy001/p/17066433.html