SqlAlchemy单表查询过滤之谜
发生背景
假设我有两个表,A 和 B
A表结构如下:
| id | content |
|---|---|
| 1 | 测试1 |
| 2 | 测试2 |
B表结构如下:
| id | aid | content |
|---|---|---|
| 1 | 1 | 测试1 |
| 2 | 1 | 测试2 |
| 3 | 2 | 测试3 |
| 4 | 1 | 测试4 |
| 5 | 2 | 测试5 |
现在我有了两个模型
|
|
奇特的一幕
当我连接A,B两个表时,发生了奇怪的一幕。
|
|
深入探究
从 sql 的角度来说,查询出来应该一定是5条数据,于是只能深入查询 sqlalchemy 源码来探究了。
源码分析
A.query 一切都是继承 Query,于是我们去查看 query.py 的源码
|
|
发现 all方法 是调用 list,也就是说 query 对象是一个可迭代的对象。因此我们可以去看 __iter__ 方法
|
|
_compile_context()是处理sql语句的方法。_execute_and_instances(context)才是真正运行的sql的语句。
|
|
- 经过调试,我们发现
result的数据依然是 5 条。 - 因此处理后的数据应该是在
loading.instances里面被处理的。 - 因此,我们继续追踪
loading.py
|
|
在这个函数里面我们需要注意如下几点:
filtered参数.single_entity参数.proc函数util.unique_list处理函数.
filtered 分析
我们是在什么时候设置 query._has_mapper_entities。不难看出这个参数是来源于 query 对象,因此我们赶快去查看 query 函数。
|
|
Query的_set_entities发现它将_has_mapper_entities设置成了 False- 因此我们只能寄希望于
_QueryEntity了.
|
|
从 _QueryEntity 的 __new__ 方法我们可以清晰的发现,它创建了一个 _MapperEntity 对象,在这个对象里面设置了 _has_mapper_entities 对象。
到这里,我们可以了解到:在处理 instantce 时我们的 filtered 为 True 了。
single_entity 分析
这个参数,其实就是看 query._entities 有多少个。
|
|
分析这个,还是从 Query 的 __init__ 方法中分析。
|
|
- 当我们经过以上代码调试的时候,发起我们的
entities只传入了一个A对象。 - 当
_MapperEntity对象被创建时,就已经设置了supports_single_entity参数。
经过以上分析:我们可以得出我们的案例,属于单个对象查询。因此我们的过滤是:
proc 函数分析
从 loading.py 的 instances 中,我们可以分析出,proc 是来自于 _MapperEntity 对象的 row_processor 方法.
|
|
从这里可以看出 _instance 来自 loading._instance_processor
|
|
note: 我们这里先不谈其他的 _instance 处理,我们这里只选取了我们这个测试案例的运行轨迹。
最后我们可以发现,proc 函数就是这里的 _instance 函数。
从上面的 session_identity_map 和 identitykey 可以看出针对(类名 + 主键)的方式来存储缓存对象,
针对相同主键的ID和类会重复使用缓存对象。
用我们的案例也就是说,A(1) 有三条数据,A(2) 有两条数据。最后会生成一个含有(5)条数据的 list。
util.unique_list 处理函数
我们可以轻易的在 _collections.py 中,看到 unique_list
|
|
经过上面的分析,我们可以清楚的知道:
seq就是我们最后得出的含有缓存对象的list;hashfunc其实就是内置的id函数;- 同一对象的
hash值是一致的;
综上所述:所以最后我们得出只含有两个对象的(list)
总结
sqlalchemy这样的做法,会使我们和SQL之间产生疑惑。- 分析得出,
sqlalchemy在做单entity查询时,会对同一条数据的对象重复利用。