首页 > 其他分享 >08-从 objects 到 QuerySet

08-从 objects 到 QuerySet

时间:2023-01-27 16:12:20浏览次数:37  
标签:__ 08 QuerySet repr objects 方法 代码 调试器

调试特点

如果 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

相关文章

  • Windows 2008 + SQLServer 2008 双机群集
    SQLserver版本要求:标准版(2个节点),企业版(16个节点)安装前,先了解相关信息:​​SQLServer2008故障转移群集入门 ​​SQLServer2008 群集是基于Windows群集:​​Windows2......
  • CF908G 题解
    题意传送门给\(x\le10^{700}\),问\(1\)到\(x\)中每个数在各数位排序后得到的数的和。答案模\(10^9+7\)。题解学到一种新鲜的转化方式,来记一下。将\(x\)的位数......
  • Windows 2008 双机群集配置(for SQLServer)
    此处配置Windows2008群集,将用于SQLserver2008双机故障转移群集,此处SQLserver的群集基于Windows群集实现。Windows2008集群更容易实现了,只要规划好IP和磁盘分配,可一直......
  • Codeforces 708 A-E1题解
    A.Meximization这道题问给一些数,如何让前缀的mex之和最大,那么首先,我们要抬mex的话肯定是要把前面都铺垫完的,所以在i位置确定的时候,i+1自然是越大越好,可以证明i+1的位......
  • 刷刷刷 Day 23 | 108. 将有序数组转换为二叉搜索树
    108.将有序数组转换为二叉搜索树LeetCode题目要求给你一个整数数组nums,其中元素已经按升序排列,请你将其转换为一棵高度平衡二叉搜索树。高度平衡二叉树是一棵满......
  • T-SQL:系统对象 SYSOBJECTS.XTYPE 各个值的含意
     SQLSERVER2008R2系统对象SYSOBJECTS.XTYPE 共有16个值,分别是:SELECTDISTINCTAO.TYPEASXTYPE,AO.TYPE_DESCFROMSYS.all_objectsAOXTYPETYPE_DESC......
  • SQL Server 2005-2008 ROW_NUMBER() 分页函数效率
    --测试数据量:2161852条declare@idatetimeset@i=GETDATE();--SQL2005-2008--开始WITHtempAS(SELECTid,title,body,ROW_NUMBER()OVER(ORDERBYid)AS'Row......
  • 洛谷 P1208混合牛奶 题解
    一道贪心算法不是很明显的题目,其实一般的递推也可以做。 大体思路:肯定优先购买单价最低的奶农的牛奶,那么就需要先根据牛奶单价进行排序,这里用结构体会更好一点。之后在......
  • P4281 [AHOI2008]紧急集合 / 聚会
    此题来到LCA较高等级运用。这道题需要自己花一些树玩玩。找到一些性质:三个点的lca一定至少有两个是一样的;更多证明集合点就是不相同的点;同时还要会求树上距离这里......
  • Objects
    We'veexplored numbers, strings, booleans, undefined and functions abit,butwehaven'treallytalkedabout objects yet.Objects areessentially:......