Django QuerySet 懒加载与缓存机制源码级拆解文档
(基于 Django 4.2.7 + CPython 3.11)
1 阅读指引
- 目标:彻底讲清"为什么第一次
for article in articles:才会真正发 SQL" - 深度:从 Python
for字节码 → Django 源码 → 数据库驱动调用栈 - 用法:可复制到内部 Wiki 或直接发博客,代码块均可运行
2 一句话结论
QuerySet 只在第一次被迭代 时才会拼 SQL、访问数据库;
之后无论再 for、len()、bool()、list() 都直接读内存缓存。
这是通过"Python 迭代协议 → QuerySet.iter() → _fetch_all()"链式触发实现的。
3 总览时序图
scss
for article in articles
├─ articles.__iter__() # QuerySet 级别
│ ├─ self._fetch_all() # 懒加载闸门
│ │ ├─ self._result_cache = list(self.iterator())
│ │ │ └─ self.iterator() # 拼 SQL + 调驱动
│ │ │ ├─ compiler.as_sql()
│ │ │ └─ cursor.execute(sql, params)
│ │ └─ 行→Model 实例
│ └─ 返回 iter(self._result_cache)
└─ next() 拿实例
( gates:只有 _fetch_all() 里 self._result_cache is None 才会走数据库)
4 环境准备
bash
git clone https://github.com/django/django.git
cd django && git checkout 4.2.7
pip install -e .
5 源码拆解
5.1 入口:QuerySet.iter
文件:django/db/models/query.py #L394
python
class QuerySet:
def __iter__(self):
self._fetch_all() # ① 保证数据已加载
return iter(self._result_cache) # ② 返回列表迭代器
5.2 闸门:_fetch_all()
文件:同文件 #L1903
python
def _fetch_all(self):
if self._result_cache is None: # 仅第一次为 None
self._result_cache = list(self.iterator())
- 之后无论
len()、bool()、list()都直接读self._result_cache list(self.iterator())会一次性把数据库行读进内存
5.3 真正拼 SQL:iterator()
文件:同文件 #L368
python
def iterator(self, chunk_size=2000):
compiler = self.query.get_compiler(using=self.db)
for row in compiler.results_iter(chunk_size=chunk_size):
yield self.model.from_db(self.db, self.model._meta.fields, row)
compiler.results_iter()→cursor.execute(sql, params)# 见下节
5.4 数据库调用点
文件:django/db/models/sql/compiler.py #L1511
python
def results_iter(self, chunk_size=None):
cursor = self.connection.cursor()
cursor.execute(sql, params) # ← 真正发 SQL
for rows in self.cursor_iter(cursor, chunk_size):
for row in rows:
yield row
6 Python 层:for 怎么调到 iter
CPython 字节码
scss
GET_ITER # 等价于 iter(obj) → obj.__iter__()
FOR_ITER # 不断 next(it)
GET_ITER 源码 (Python/ceval.c) 固定走 PyObject_GetIter → tp_iter → __iter__
因此任何可迭代对象 只要实现 __iter__ 就会被 for 触发;
QuerySet 正是利用这一点把"迭代"挂钩到 _fetch_all()。
7 缓存验证实验
python
from django import db
from blog.models import Article
articles = Article.objects.filter(status='published')
print(articles._result_cache) # None
for a in articles: # 第一次迭代
pass
print(len(db.connection.queries)) # 1 条 SQL
print(articles._result_cache) # [<Article:...>, ...]
for a in articles: # 第二次迭代
print(a.title) # 不再发 SQL
print(len(db.connection.queries)) # 仍是 1 条
8 常见误区速查表
| 操作 | 是否立即查库 | 备注 |
|---|---|---|
Article.objects.all() |
否 | 只创建 QuerySet |
qs.filter(...) |
否 | 返回新 QuerySet |
qs.order_by(...) |
否 | 同上 |
qs[10] |
是 | 切片会触发 _fetch_all |
bool(qs) |
是 | 会调用 __bool__→_fetch_all |
len(qs) |
是 | 会调用 __len__→_fetch_all |
list(qs) |
是 | 直接 list() 会迭代 |
for x in qs: |
是 | 第一次迭代 |
再次 for x in qs: |
否 | 用缓存 |
9 小结口诀
QuerySet 是"惰性链表 "------
链上每一步都只是"记录条件",
直到第一次迭代才把整个链编译成 SQL 拉进内存 ;
拉完后结果装进 _result_cache,
往后任何姿势的遍历都不再打扰数据库。