Django 入门与环境搭建
1. 章节介绍
本章带领大家在本地快速搭建 Django 3.2 运行环境,理解 Django 的 MVT 架构与两种主流开发模式(前后端分离 / 不分离),为后续项目实战与源码阅读打下基础。内容侧重"能跑起来 + 能看明白",面试常问"Django 生命周期、MVT 与 MVC 区别、分离与不分离的取舍",均需从本讲引申。
| 核心知识点 | 面试频率 | 备注 |
|---|---|---|
| Django 版本选型与 pip 指定安装 | 中 | 3.2 LTS 稳定,4.x 新特性 |
| MVT 各层职责与数据流 | 高 | 必问,需画图 |
| 路由 → 视图 → 模板/序列化器 → 响应 完整生命周期 | 高 | 常让手写流程图 |
| 前后端分离 vs 不分离场景取舍 | 高 | 结合 RESTful、SEO、团队技能 |
| ORM 模型类与数据库交互理念 | 高 | "不写 SQL" 是亮点 |
2. 知识点详解
2.1 环境准备
- Python ≥3.8 均可,本课程统一 3.10
- 虚拟环境:
python -m venv venv && source venv/bin/activate - 固定版本安装:
pip install Django==3.2.18- 不指定会装最新 4.x,与课程代码可能出现兼容差异
- 验证:
python -m django --version→ 应输出 3.2.18
2.2 官方文档导航技巧
- 地址:https://docs.djangoproject.com/
- 右上角同时切换"版本 3.2"+"Language 中文",可离线保存 PDF 备用
- 重点先行:
- Tutorial(polls 项目)
- Model layer
- View layer
- Template layer
- Django REST framework(后续章节)
2.3 MVT 架构拆解
- M(Model):ORM 映射,一个类 = 一张表,一个实例 = 一行数据
- V(View):业务逻辑 + 请求响应枢纽,等价于 MVC 的 C
- T(Template):仅当"前后端不分离"时渲染 HTML;分离场景下由前端框架接管,Django 只负责 JSON
数据流(面试口述版):
浏览器 → Web 服务器(Nginx) → WSGI/ASGI → Django 路由层(urls.py)
→ 视图函数(views.py) 【可选:查询 Model】
→ 渲染成 Template 或序列化成 JSON
→ HttpResponse / JsonResponse → 浏览器
2.4 开发模式对比
| 维度 | 不分离(传统 SSR) | 分离(SPA + RESTful) |
|---|---|---|
| 页面渲染 | Django Template | Vue/React/Angular |
| 响应格式 | text/html | application/json |
| SEO | 友好 | 需 SSR/SSG 额外方案 |
| 团队技能 | 全栈 Python 即可 | 前端 + 后端双栈 |
| 课程后续实战 | 先学 Template 夯实 MVT,再用 DRF 转分离 |
2.5 ORM 初印象
- 不写 SQL:Model.objects.filter(username='alice').update(age=18)
- 迁移命令:
makemigrations→migrate - 默认支持 SQLite,零配置即可启动;生产切 PostgreSQL/MySQL 仅需改
DATABASES字典
3. 章节总结
- 用
pip install Django==3.2.18锁定长期支持版 - 官方文档切"3.2+中文"作为第一手资料
- Django 按 MVT 分层:Model 管数据、View 管业务、Template/JSON 管表现
- 路由 → 视图 → (模型+模板/序列化器) → 响应 是面试必画生命周期
- 根据团队技能与 SEO 需求,在"模板渲染"与"DRF 分离"间灵活切换
4. 知识点补充
4.1 补充 5 个高频延伸
- WSGI vs ASGI:Django 3.2 默认 WSGI,4.x 推荐 ASGI 以支持 WebSocket
- settings 拆分:
base.py/dev.py/prod.py12-Factor 配置管理 - App 划分原则:按业务分 app,避免"万能 app",保持解耦
- Django 中间件:5 个钩子方法,典型用途日志、权限、CORS
- Django 自带后台:一句
admin.site.register(Model)生成运营级后台,演示原型利器
4.2 最佳实践(≥300 字)
生产环境部署 Django 时,务必"先静态后动态、先缓存后数据库"。
- 静态文件:使用
python manage.py collectstatic统一收集到STATIC_ROOT,再由 Nginx 直接托管,避免 Django 处理GET /static/压力 - 反向代理:Nginx → gunicorn/uwsgi 的 UNIX socket,比 127.0.0.1:8000 少一次 TCP 握手;gunicorn workers 数设
CPU×2+1,并开启--preload预加载代码 - 数据库连接池:Django 3.2 原生不支持连接池,使用
django-db-geventpool或pgbouncer防止高并发下"too many connections" - 缓存层:对读多写少接口加
cache_page(timeout=60*15),或手动cache.set(key, data, 900);更新数据时先删缓存再写库,保证最终一致 - 日志:统一
dictConfig,区分django.request/django.db.backends,日志落盘 + ELK 收集,方便链路追踪 - 安全:关闭
DEBUG=True;ALLOWED_HOSTS精确到域名;使用django-environ把 SECRET_KEY 放到环境变量;HTTPS 强制 HSTS - 监控:Prometheus + django-prometheus 暴露
/metrics,配合 Grafana 面板实时观察 5xx、响应时间、SQL 平均耗时 - 灰度:利用 Nginx 的
split_clients按 Cookie 或 IP 分流,先灰 10% 流量到新版本,观察 Sentry 无异常后再全量
遵循以上 8 步,可在 1 万并发场景下把 P99 响应时间控制在 200 ms 以内,同时保持横向扩展能力
4.3 编程思想指导(≥300 字)
Django 的" batteries-included "背后体现的是"约定优于配置"与"DRY(Don't Repeat Yourself)"思想。
首先,把共性需求沉淀到框架:路由、ORM、认证、后台、缓存、国际化开箱即用,让开发者聚焦业务而非基础设施。
-
这启示我们,在企业内部也应持续抽象公共组件------比如统一支付、统一消息、统一网关,业务线只需"声明式"调用,降低边际成本 。
-
其次,MVT 分层本质是"关注点分离":Model 只关心数据一致性,View 只关心业务编排,Template/Serializer 只关心表现格式。
-
写代码时,应让每层单向依赖,View 可以依赖 Model,但 Model 绝不 import View;同理,微服务间也要避免循环调用,保持 DAG 依赖图
-
再次,ORM 屏蔽 SQL 差异,但"抽象泄露"定律提醒我们,复杂查询仍需回归 SQL 或原生游标;
在性能敏感场景,先 select_related / prefetch_related 预取,再 only() 延迟加载,必要时用 RawSQL 或 extra()。
这告诉我们:框架是 90% 场景的银弹,剩下 10% 必须理解底层,才能精准调优
- 最后,Django 的"可插拔 app"设计让功能像积木一样拼装。我们在做中大型项目时,也应把"用户、订单、库存"拆成独立 app(甚至独立服务),通过接口或事件总线通信;只要边界清晰,未来从单体迁到微服务,只需把 app 复制到新仓库,改入口与配置即可,业务代码几乎零改动
总结一句话:先用框架思维解决通用问题,再用底层思维解决剩余痛点;分层、抽象、单向依赖、可插拔,是任何规模软件保持长期可维护的四大支柱
5. 程序员面试题
简单题
Q:Django 3.2 属于什么版本策略?为何企业倾向使用 LTS?
A:3.2 是长期支持版(LTS),官方提供 3 年安全更新;企业使用可减少升级成本,保证稳定性与合规需求
中等题
Q:画出 MVT 数据流,并指出各层等价于 MVC 的哪一部分
A:浏览器 → urls → view(MVC 的 C) → model(M) → template(V) → response;MVT 的 V 等价 MVC 的 C,T 等价 V,M 保持不变
Q:列举至少 3 种减少 Django ORM N+1 查询的方法
A:1) select_related 做连表 2) prefetch_related 做双查询+缓存 3) only/defer 延迟加载字段 4) values/values_list 取字典/元组 5) 手动 RawSQL 一次查完
高难度题
Q:在高并发场景下,Django 默认的 SQLite 会出现哪些瓶颈?如何在不改代码的前提下切换到 PostgreSQL?
A:SQLite 瓶颈:写锁整张表,无并发写;无网络端口,无法做读写分离;单文件达 2 GB 后性能骤降
切换步骤:安装 psycopg2 → 建库 create database dbname → 改 settings 的 DATABASES 引擎为 django.db.backends.postgresql,填 NAME/USER/PW/HOST/PORT → 执行 python manage.py migrate 即可;因 ORM 屏蔽差异,业务代码无需改动
Q:解释 Django 中间件 process_request / process_response 执行顺序,并给出实现"接口耗时日志"的伪代码
A:请求自上而下穿中间件栈,返回时逆序;process_request 越早注册越先执行,process_response 越晚注册越先执行
伪代码:
python
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
import time, logging
start = time.time()
response = self.get_response(request)
cost = (time.time() - start) * 1000
logging.info(f"{request.method} {request.path} {response.status_code} {cost:.2f}ms")
return response
注册到 MIDDLEWARE 列表即可统计全链路耗时,方便定位慢查询
Django 模型(Model)系统精讲
1. 章节介绍
本章在上节"Django 项目开发总览"基础上,深入讲解 MVP 架构中的 M(Model)层:从项目/应用创建、MySQL 连接配置,到 ORM 字段类型、常用选项、迁移执行等完整闭环。掌握后,可独立设计数据库、编写模型类并落地成表,为后续视图与模板奠定数据基础。
| 核心知识点 | 面试频率 |
|---|---|
| 项目与应用创建流程 | 中 |
| settings.py 注册应用 | 高 |
| MySQL 数据库配置(ENGINE / NAME / USER / PASSWORD / HOST / PORT) | 高 |
| 安装 mysqlclient / PyMySQL 驱动 | 中 |
| ORM 字段类型(AutoField、CharField、TextField、DateTimeField、DecimalField 等) | 高 |
| 常用字段选项(primary_key、max_length、default、null、blank、unique、db_index、verbose_name、help_text) | 高 |
| 模型迁移(makemigrations / migrate) | 高 |
| 模型层最佳实践与编程思想 | 中 |
2. 知识点详解
2.1 项目 & 应用创建
- 创建项目:
django-admin startproject project_name - 进入项目根目录,创建应用:
python manage.py startapp app_name - 将应用注册到
INSTALLED_APPS,否则模型无法被识别。
2.2 切换 MySQL 数据库
-
手动建库:
CREATE DATABASE dbname CHARACTER SET utf8mb4; -
安装驱动:
-
官方推荐:
pip install mysqlclient(需系统依赖) -
纯 Python 备选:
pip install PyMySQL,并在__init__.py中:pythonimport pymysql pymysql.install_as_MySQLdb()
-
-
修改 DATABASES:
pythonDATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'web02', 'USER': 'root', 'PASSWORD': 'mysql', 'HOST': '127.0.0.1', 'PORT': 3306, 'OPTIONS': {'charset': 'utf8mb4'}, } }
2.3 ORM 字段速查
| 场景 | 字段类 | 备注 |
|---|---|---|
| 主键自增 | AutoField(primary_key=True) |
可省略,默认 id |
| 短字符串 | CharField(max_length=xx) |
必须带 max_length |
| 长文本 | TextField() |
超过 4k 字符推荐 |
| 整数 | IntegerField() / PositiveIntegerField / BigIntegerField |
按大小选 |
| 精确金额 | DecimalField(max_digits=10, decimal_places=2) |
金融必备 |
| 浮点 | FloatField() |
近似值,慎用 |
| 布尔 | BooleanField() |
数据库层面 0/1 |
| 日期时间 | DateTimeField(auto_now_add=True) |
创建时自动写入 |
| 文件/图片 | FileField(upload_to='') / ImageField() |
需配置 MEDIA |
2.4 常用字段选项
primary_key=True→ 主键max_length=120→ 字符串长度default=0/default=timezone.now→ 默认值null=True→ 数据库允许 NULLblank=True→ 表单校验允许为空unique=True→ 唯一索引db_index=True→ 普通索引verbose_name='中文名'→ 后台显示help_text='提示'→ 表单提示
2.5 生成与执行迁移
bash
python manage.py makemigrations # 生成迁移文件
python manage.py migrate # 落地成表
迁移记录表 django_migrations 会记录版本,回滚用 migrate app zero。
3. 章节总结
- 先建项目→应用→注册应用;
- 手动建 MySQL 库,改 ENGINE 与连接参数,装驱动;
- 在 models.py 中写类 → 选字段 → 加选项;
- makemigrations + migrate 一键成表;
- 后续只需改模型再迁移,即可持续演进表结构。
4. 知识点补充 & 最佳实践
4.1 补充知识
Meta内部类:指定db_table、indexes、ordering、verbose_name_plural等。- 复合索引:
models.Index(fields=['status', '-created'])提升多列查询速度。 - 软删除:新增
is_deleted字段,重写delete()方法做逻辑删除,避免误删。 - 分表策略:单表千万级后,可按时间/哈希拆分,结合 Django 多数据库路由。
- 自定义字段:继承
Field实现JSONField(Django<3.1 时)、EncryptedCharField等。
4.2 最佳实践(≥300 字)
"模型是项目的地基"------设计阶段多花 10 分钟,能避免上线后 10 倍的返工。
- 命名规范:表名用复数小写
news_article,字段用蛇形pub_date;类名用驼峰NewsArticle,保持一致性。 - 主键策略:默认自增 id 即可;分布式场景再改用
BigAutoField或雪花 ID,避免后期迁移痛苦。 - 货币与精度:金额一律
DecimalField,禁止FloatField防止精度丢失;同时配合positive校验。 - 时间字段:创建时间
auto_now_add=True,更新时间auto_now=True,确保审计轨迹。 - 索引意识:对外键、查询条件、排序字段建索引;但控制总数,写多读少场景需权衡。
- 迁移纪律:每功能一小步,禁止手动改表结构;迁移文件入库,方便多环境回放。
- 数据种子:用
fixtures或factory_boy生成测试数据,保持 CI 可重复。 - 安全默认:
null=False, blank=False先收紧,必要时再放开;敏感字段加密后再入库。 - 版本兼容:字段新增先允许空,代码双版本兼容,灰度完成后再补非空约束。
- 文档同步:在
help_text与verbose_name写清业务含义,自动生成后台文档,降低沟通成本。
4.3 编程思想指导(≥300 字)
ORM 的核心思想是"把关系数据库映射成对象",让开发者用面向对象思维写 SQL,从而提高抽象层级与可维护性。
- 抽象优先:先思考"对象是什么、职责是什么",再决定表结构;而非先画表再硬套对象。
- 单一职责:一个模型只负责一块业务域;不要把用户、订单、日志揉在一个类里。
- 充血模型:把与自身数据强相关的行为(如
get_absolute_url()、increase_views())放到模型类/管理器里,避免 Service 层臃肿。 - 延迟加载:利用 QuerySet 懒查询特性,先过滤再取值,减少 N+1;必要时用
select_related/prefetch_related批量Join。 - 领域即语言:好的模型就是业务语言,
Article.objects.published().hot()让代码自解释。 - 迁移即版本:把每一次 schema 变更当成一次 commit,可回滚、可 review、可追踪。
- 性能意识:ORM 不是银弹,复杂报表仍可用原生 SQL 或视图,但要把结果再封装成模型,保持接口统一。
- 测试护航:为模型写单元测试,验证约束、默认值、信号触发;用
TransactionTestCase隔离测试数据。 - 演进式:业务变化时,宁可加新字段也不要改旧字段含义;废弃字段先标记
deprecated,数个版本后再清理。 - 多数据库:读写分离、分片、异地多活时,用 Django 数据库路由把细节藏在框架层,上层业务代码零感知。
5. 程序员面试题
-
简单
创建 Django 模型
Book,包含书名、作者、价格、出版日期,请写出模型代码。pythonfrom django.db import models class Book(models.Model): title = models.CharField('书名', max_length=100) author = models.CharField('作者', max_length=50) price = models.DecimalField('价格', max_digits=8, decimal_places=2) pub_date = models.DateField('出版日期') class Meta: db_table = 'book' -
中等
解释
null=True与blank=True的区别,并举例何时同时用。
null=True表示数据库字段允许存储NULL值;blank=True表示 Django 表单验证时允许该字段为空(即非必填)。二者作用层面不同,前者针对数据库,后者针对表单逻辑。例如,一个可选的过期时间字段可以同时设置两者:pythonexpire_time = models.DateTimeField(null=True, blank=True) -
中等
如何在已有百万级数据的
User表上安全新增非空nickname字段?-
首次迁移:新增字段并允许为空:
pythonnickname = models.CharField(max_length=30, null=True, blank=True) -
编写数据填充脚本,为已有记录设置默认值(如"用户123")或从其他字段生成;
-
第二次迁移:修改字段为非空(
null=False); -
为确保安全,上线前应进行灰度发布,并在代码中兼容新旧字段状态。
-
-
困难
说明 Django 迁移系统工作原理,并手写一个自定义迁移文件,给
Article表的status字段创建复合索引(status, -created)。
原理 :Django 根据模型定义生成迁移文件(.py),记录数据库结构变更操作。执行migrate时,Django 读取未应用的迁移文件,执行对应 SQL,并将已执行记录写入django_migrations表以避免重复执行。
自定义迁移步骤:bashpython manage.py makemigrations --empty your_app_name然后编辑生成的空迁移文件:
pythonfrom django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('your_app_name', '000x_previous_migration'), ] operations = [ migrations.AddIndex( model_name='article', index=models.Index(fields=['status', '-created'], name='idx_status_created') ), ] -
困难
线上出现大量慢查询,定位到
Order.objects.filter(user=u).order_by('-created')导致全表扫描,如何优化?-
确保
user字段已建索引(ForeignKey默认有索引,但可显式确认); -
添加复合索引覆盖查询条件和排序字段:
pythonclass Meta: indexes = [ models.Index(fields=['user', '-created'], name='order_user_created_idx') ] -
执行迁移后,使用
EXPLAIN验证 SQL 是否命中索引; -
若数据量极大,可考虑分页优化(如基于游标的分页)、冷热数据分离,或使用覆盖索引减少回表查询。
-
Django ORM 模型类设计与多表关系实战
1. 章节介绍
本章以"新闻网站"业务为例,演示如何在 Django 中把数据库 ER 图 转化为可迁移的 Model 代码,重点解决:
- 模型类字段选型、常用参数、help_text 等工程细节
- 一对一、一对多、多对多三种关系的识别、表达与迁移落地
- 自动生成中间表、反向查询、自关联等面试高频点
| 核心知识点 | 面试频率 | 备注 |
|---|---|---|
| 模型类字段类型与选项 | 高 | max_length、help_text、default、verbose_name |
| 三种表关系及 Model 表达 | 高 | 1-1 / 1-N / M-N,ForeignKey、ManyToManyField 位置与参数 |
| 迁移命令与中间表机制 | 高 | makemigrations / migrate,M-N 自动第三张表 |
| 自关联 & 反向查询 | 中 | related_name、self、select_related / prefetch_related |
| Model 最佳实践(命名、索引、db_table) | 中 | 代码风格、性能、可维护性 |
2. 知识点详解
2.1 模型类字段选型
- CharField → 短字符串(标题、名称),必须给
max_length - TextField → 长文本(正文、评论),不限长度
- IntegerField / PositiveIntegerField → 计数器(阅读量、评论数)
- DateTimeField → 时间戳(auto_now / auto_now_add)
- BooleanField / DecimalField → 布尔、金额等高精度场景
常用选项
python
title = models.CharField(
max_length=100,
verbose_name='标题',
help_text='用于后台显示与提示',
db_index=True # 面试加分:主动建索引
)
2.2 三种关系速记
| 关系 | 业务语义 | Django 表达 | 字段放哪端 |
|---|---|---|---|
| 一对一 1-1 | 一条新闻对应一条扩展信息 | OneToOneField(扩展表) | 任意端 |
| 一对多 1-N | 一个类型下有多篇新闻 | ForeignKey('多'端) | 多端(News) |
| 多对多 M-N | 一篇新闻可同时属于多个类型 | ManyToManyField | 任意端,自动中间表 |
代码模板
python
# 新闻类型表(一)
class NewsCategory(models.Model):
name = models.CharField('类型名称', max_length=20, unique=True)
# 新闻表(多)
class News(models.Model):
title = models.CharField('标题', max_length=100)
content = models.TextField('正文')
read_cnt = models.PositiveIntegerField('阅读量', default=0)
cmt_cnt = models.PositiveIntegerField('评论数', default=0)
# 1-N:一个类型对应多篇新闻
category = models.ForeignKey(
NewsCategory,
on_delete=models.CASCADE,
related_name='news_set'
)
# M-N:一篇新闻可打多个标签
tags = models.ManyToManyField('Tag', blank=True)
2.3 迁移与中间表
python manage.py makemigrations# 生成迁移文件python manage.py migrate# 真正建表- M-N 关系会自动生成
app_model1_model2中间表,包含两列*_id
3. 章节总结
- 先画 ER 图 → 再写 Model → 再迁移;字段选型、help_text、索引一步到位
- 1-N 用 ForeignKey 放在"多"端;M-N 用 ManyToManyField 任意端;1-1 用 OneToOneField
- 迁移后务必检查数据库,确认中间表、外键索引、字段属性符合预期
4. 知识点补充
4.1 额外 5 个高频考点
-
related_name & related_query_name:反向查询别名,避免冲突
-
on_delete 选项:CASCADE / PROTECT / SET_NULL / SET_DEFAULT 面试必问
-
through 自定义中间表:需要给关系加额外字段(如"打标签时间")
-
self 自关联:评论回复、无限级分类
pythonclass Comment(models.Model): ... parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True) -
Meta 内部类:db_table、ordering、indexes、unique_together、verbose_name
4.2 最佳实践(≥300 字)
场景:日均千万级阅读的新闻站点,要求
- 后台可实时查看"某类型下阅读量 Top100"
- 外键不能拖慢查询
- 模型改动可灰度迁移
实践步骤
- 字段设计
- 读/写分离:read_cnt 用 PositiveIntegerField,避免负值;同时建 read_cnt_idx 联合索引
(category_id, -read_cnt) - 标题长度按业务 90% 分位值给 120,不盲目 255;content 用 TextField + 额外 ES 索引,不建 DB 索引
- 读/写分离:read_cnt 用 PositiveIntegerField,避免负值;同时建 read_cnt_idx 联合索引
- 关系设计
- 1-N 外键显式声明
db_index=True(Django 4.1+ 已默认,但显式写出可读性高) - M-N 用默认中间表即可;若需"加标签时间"则自定义 through 表,并加
created_at字段
- 1-N 外键显式声明
- 迁移策略
- 大表加字段使用
AddField(migrations.AddField(...), preserve_default=False)+ 灰度双写 - 新建索引使用
migrations.AddIndex(..., condition='CONCURRENTLY')(PostgreSQL)避免锁表
- 大表加字段使用
- ORM 查询
- 列表页用
select_related('category')一次性拿外键,N+1 → 1 查询 - Top100 直接走覆盖索引:
News.objects.filter(category=xxx).order_by('-read_cnt')[:100]
- 列表页用
- 代码规范
- 统一 verbose_name / help_text,后台自动生成友好表单
- 模型文件名 models/news.py,避免单文件过大
遵循以上规范,可在高并发 与快速迭代之间取得平衡,同时让面试答案"有数据、有案例、有量化"。
4.3 编程思想指导(≥300 字)
-
"先关系,后字段"
很多开发者一上来就写字段,结果外键放错端。正确顺序:先识别实体 → 确定关系 → 再补字段。ER 图哪怕手绘,也能让 ForeignKey 位置一目了然。
-
"把规则显式化"
on_delete=models.PROTECT比 CASCADE 安全,但很多人图方便直接 CASCADE。把业务规则显式写到代码里,既自文档又防误操作。 -
"迁移可逆"
每次 makemigrations 后,本地先 migrate → 再 migrate zero,确认可回滚。生产环境灰度发布才能做到"出问题 30 秒内回滚"。
-
"性能提前设计"
面试常问"如果数据量扩大 100 倍怎么办?"------在模型层就给出答案:
- 索引、联合索引、覆盖索引
- 自增 ID 与雪花 ID 的权衡
- 分库分表中间件与 ShardingKey 选取
-
"ORM 是 DSL,不是黑盒"
用
queryset.query打印 SQL,用explain分析索引;把 ORM 当"生成 SQL 的 DSL",而不是"屏蔽 SQL 的魔法"。理解其生成规则,才能写出既优雅又高效的代码。
5. 程序员面试题
-
简单
写一段
News模型,包含标题、正文、发布时间,并给标题加索引。pythonfrom django.db import models class News(models.Model): title = models.CharField(max_length=100, db_index=True) content = models.TextField() pub_time = models.DateTimeField(auto_now_add=True) -
中等
1-N 关系
ForeignKey应该放在哪一端?为什么?应放在"多"的一端。例如,一篇新闻属于一个分类,则
News模型中包含指向Category的ForeignKey。这样在数据库层面只需在news表加一个外键列,无需额外中间表,且 Django ORM 支持通过反向关系(如category.news_set.all())便捷地查询所有相关新闻。 -
中等
ManyToManyField实际会生成几张表?如何自定义中间表?默认会生成 1 张中间表,仅包含两个外键字段。若需在中间表中添加额外字段(如创建时间、备注等),应使用
through参数自定义中间模型:pythonclass News(models.Model): tags = models.ManyToManyField('Tag', through='NewsTag', through_fields=('news', 'tag')) class Tag(models.Model): name = models.CharField(max_length=50) class NewsTag(models.Model): news = models.ForeignKey(News, on_delete=models.CASCADE) tag = models.ForeignKey(Tag, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) # 额外字段 -
困难
如果新闻表数据量 10 亿,要按"分类+发布时间"分页查询,如何设计索引与模型?
-
在数据库层面创建联合索引:
(category_id, -pub_time),确保查询和排序高效; -
避免深度分页(如
OFFSET 1000000),改用基于游标的分页(如WHERE category_id = X AND pub_time < last_seen_time); -
使用覆盖索引(
INCLUDE或选择必要字段)减少回表; -
考虑按
category_id对表进行水平分区(或分库分表),分散 I/O 压力; -
在 ORM 查询中限制字段,例如:
pythonNews.objects.filter(category=cat).order_by('-pub_time').only('id', 'title')[:20]
-
-
困难
解释
on_delete=models.SET(func)的用途,并给出伪代码。
on_delete=models.SET(func)表示当被引用的对象被删除时,Django 会调用func函数,并将外键字段设为该函数的返回值。常用于"软删除"后自动迁移关联数据。
伪代码示例:pythondef get_default_category(): return NewsCategory.objects.get_or_create(name='默认分类')[0] class News(models.Model): category = models.ForeignKey( NewsCategory, on_delete=models.SET(get_default_category) )当某
NewsCategory被删除时,其关联的News实例会自动指向"默认分类"。注意:SET()中的函数不能带参数,且应在模块顶层定义(避免序列化问题)。
Django 模型类 Meta 选项与后台管理最佳实践
1. 章节介绍
本节在"模型类字段定义"基础上,深入讲解 Meta 元选项 与 admin 后台配置 两大主题。通过自定义表名、中文展示、后台列表等实战细节,让模型在数据库与后台两侧均拥有友好、可维护的形态,为后续查询与业务开发打下坚实基础。
| 核心知识点 | 面试频率 | 一句话速览 |
|---|---|---|
| db_table 指定表名 | 高 | 避免默认"app_model"长表名,保持与旧系统或团队规约一致 |
| verbose_name / verbose_name_plural | 中 | 让 Django Admin 显示中文,不再看到"News Info object(1)" |
| str 魔术方法 | 高 | 决定 print(obj)、Admin 下拉框、Debugger 中的可读字符串 |
| Admin 模型管理类(ModelAdmin) | 高 | list_display、list_filter、search_fields 三剑客必会 |
| 创建超级用户、汉化、时区 | 中 | python manage.py createsuperuser + LANGUAGE_CODE / TIME_ZONE |
2. 知识点详解
2.1 自定义表名 db_table
- 默认规则:
<app_label>_<model_name>小写,易变长、难对齐。 - 指定方式:在模型内部再写
class Meta: db_table = 'news_info'。 - 迁移前确定:表一旦生成再改会涉及数据搬迁,需提前评审。
- 与数据库团队对齐:遗留系统或 DBA 要求统一前缀/下划线风格时必用。
2.2 单/复数可读名 verbose_name*
python
class Meta:
verbose_name = '新闻信息表' # 单数
verbose_name_plural = '新闻信息表' # 复数,不指定默认加 s
- 影响 Admin 首页左侧导航与列表页标题。
- 支持 gettext 国际化:
verbose_name = _('news')为多语言铺路。
2.3 模型对象的"脸"------str
python
def __str__(self):
return self.title # 或 f'{self.id}-{self.title[:20]}'
- 触发场景:Admin 下拉多选、Shell 调试、Django Debug Toolbar。
- 性能注意:不要外键深查询,避免 N+1。
2.4 Admin 模型管理类
python
@admin.register(News)
class NewsAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'read_count', 'comment_count', 'type')
list_filter = ('type',) # 右侧过滤
search_fields = ('title', 'content') # 顶部搜索
list_per_page = 20 # 分页
# 编辑页也控制
fields = ('title', 'content', 'type', 'read_count', 'comment_count')
- 注册方式:装饰器优于
admin.site.register(),可一处写完。 - 外键多选:ManyToMany 在 Admin 默认用复选框,按住 Ctrl 多选;可改 filter_horizontal 更友好。
2.5 后台汉化 & 时区
python
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True # 若用 datetime 本地时间需 `django.utils.timezone.now`
- 汉化包括 Admin 系统界面、验证错误提示、日期格式。
- 时区决定 Admin 里"创建时间"是否+8h,生产环境务必与数据库一致。
3. 章节总结
- 用
Meta.db_table让表名符合团队规约。 - 用
verbose_name*让 Admin 显示中文,告别"Object"。 - 给模型写
__str__,让调试、下拉框可读。 - 自定义
ModelAdmin,把常用字段、过滤、搜索一次性配好。 - 创建超级用户并汉化,为运营/产品提供友好录入环境。
4. 知识点补充
4.1 额外必知 Meta 选项
- ordering = ['-id']:默认倒序,减少视图层 .order_by() 重复代码。
- indexes = [models.Index(fields=['created_at'])]:复合索引,加速时间范围查询。
- abstract = True:公共父模型,不产生表,常用于时间戳、软删除基类。
- app_label = 'cms':模型文件不在标准 apps.py 时显式归属。
- constraints = [models.UniqueConstraint(...)]:联合唯一约束,替代 unique_together。
4.2 最佳实践:可维护的"新闻信息"模型设计(≥300 字)
在真实项目中,模型设计第一目标是"可读 + 可演 + 可迁移"。以新闻系统为例,建议采用如下结构:
- 表名统一前缀:公司规范
cms_news,cms_news_type,避免与支付、订单表混杂。 - 主键用默认自增 id,但暴露给前端的用 hashids 编码,防止爬虫顺序遍历。
- 状态字段用 SmallIntegerField + choices,如 STATUS_CHOICES = ((1, '草稿'), (2, '已发布')...;Admin 内用 list_filter 快速筛选。
- 时间字段统一用
DateTimeField(auto_now_add=True)与auto_now=True,并在 Meta.ordering 里写 ['-publish_at'],保证默认列表最新在前。 - 正文若超 5k 字,拆独立 NewsContent 模型,一对一,避免大文本拖慢列表查询。
- 多对多通过显式中间表
NewsTypeRelation加权重字段,方便后续"主类型""次类型"排序。 - 阅读量、点赞量等高并发字段,单独拆 Redis 计数,通过定时任务回写 DB,模型只保留快照字段。
- 国际化:标题、正文存 JSONField(PostgreSQL)或单独 NewsI18n 表,按语言 code 查询。
- 软删除:加
is_deleted + deleted_at,重写自定义 Manager 过滤掉已删数据,Admin 另配 DeletedFilter 供运营恢复。 - 版本化:每发布一次生成 NewsVersion,支持一键回滚;Admin 内用 TabularInline 展示历史版本。
这样,模型上线后可平滑支持:灰度发布、多语言、高并发读、数据恢复,且迁移文件清晰,字段语义明确,新人 5 分钟即可读懂。
4.3 编程思想指导:模型即接口(≥300 字)
Django 模型不只是"数据库表翻译器",更是 面向业务对象的接口。写好模型,等于给整个项目定下"领域方言"。思想要点:
- 领域优先:先和业务方对齐"新闻""类型""标签""栏目"概念,再落地字段;切忌一上来就考虑索引、缓存。
- 高内聚低耦合:模型只负责"自己"数据与基础行为(如
__str__、get_absolute_url)。发邮件、调用第三方 API 放到 service 层或 Celery 任务。 - 显式优于隐式:verbose_name、help_text、choices 都写全,Admin 里就能自解释;半年后你自己也忘了 0/1/2 代表啥。
- 默认安全:BooleanField 默认值务必给出;CharField 加 max_length 限制;DecimalField 用货币场景,避免 Float 精度坑。
- 面向查询设计:经常 list_filter 的字段就加 db_index;模糊搜索用 PostgreSQL 的 GinIndex + icontains,MySQL 则考虑全文索引或 ES。
- 演进式:小步快跑,每加一个字段就写迁移、写 Admin、写单元测试,避免"一口气加 20 个字段"导致回滚困难。
- 元编程慎用:重写
__getattribute__、动态给类挂字段,会让调试难度指数级上升;除非写框架,否则别炫技。 - 文档即代码:在模型 docstring 里写"业务主键规则、单位、是否软删、是否写 Redis",自动生成 Sphinx 文档,永远保持同步。
把模型当"API 第一公民"来尊重,后续无论是 DRF 序列化、GraphQL、还是 gRPC,都能复用同一套字段语义与校验逻辑,极大降低沟通与重构成本。
5. 程序员面试题
| 难度 | 题目 | 参考答案 |
|---|---|---|
| 简单 | 如何在 Django 模型里把表名从默认的 app_model 改成 t_news? |
在模型内部写: class Meta: db_table = 't_news' |
| 中等 | 为什么要在模型里实现 __str__?请写出一段示例代码并说明其在 Admin 中的作用。 |
让对象可读: def __str__(self): return self.title Admin 下拉框、列表过滤、调试 shell 都能看到标题而非"News object(1)"。 |
| 中等 | 列出三种常用的 ModelAdmin 属性,并说明各自用途。 | 1. list_display:列表页要显示的列; 2. list_filter:右侧快速过滤; 3. search_fields:顶部搜索框。 |
| 困难 | 如果新闻表数据量过亿,列表页直接 list_display = ('id','title','read_count') 会出现性能问题,请给出三种优化思路并说明如何实现。 |
① 只 select 必要字段(queryset.only)+ 分页;② 读并发量写入 Redis,定时批量回写,列表页走 Redis 缓存;③ 分区表/分库分片,按时间或 hash 分片,Admin 后台接入 Elasticsearch 提供搜索,列表页走 ES 聚合。 |
| 困难 | 需求:新闻类型需要支持"多语言名称",但数据库仍用单表。如何设计模型与 Admin,使得切换 LANGUAGE_CODE 时 Admin 里类型名称实时变化? |
模型用 JSONField 存 {"zh": "国际", "en": "International"};重写 __str__ 根据 get_language() 返回对应语言;Admin 里仍注册原模型,无需额外表即可实现多语言展示。 |
---# [P05]框架基础:模型类查询的方法.ai-zh.srt 字幕整理结果
Django ORM 基础查询五件套
1. 章节介绍
本节以 Django 自带的 ORM 为例,讲解 5 个最常用、面试必考的"单表查询"方法:
get / filter / all / exclude / order_by。掌握它们的行为差异、返回值类型与异常规则,是后续复杂查询、性能优化、甚至线上排障的前提。
| 知识点 | 面试频率 | 一句话定位 |
|---|---|---|
| get 唯一查询 | 高 | 找不到 or 找到多条都抛异常 |
| filter 条件过滤 | 高 | 永远返回 QuerySet,不会抛异常 |
| all 全表拉取 | 中 | 等价于"无 where"的 SQL |
| exclude 反向过滤 | 中 | where not 的 ORM 写法 |
| order_by 排序 | 高 | 默认升序,-字段 降序 |
| QuerySet 三大特征(惰性、缓存、链式) | 高 | 理解"真正执行 SQL 的时机" |
2. 知识点详解
2.1 get(**kwargs)
- 语义:必须且只能拿到 一条 记录。
- 返回值:模型实例(Model 对象)。
- 异常:
Model.DoesNotExist------ 0 条。Model.MultipleObjectsReturned------ ≥2 条。
- 使用场景:主键或业务唯一键查询,如
User.objects.get(pk=uid)。 - 注意:不要放在
try/except外做"存在性"判断,代价高。
2.2 filter(**kwargs)
-
语义:按条件过滤,不限制条数。
-
返回值:QuerySet(可能为空,但不会抛异常)。
-
支持链式:多次
.filter()等价于 SQL 的 AND。 -
常见写法:
pythonqs = News.objects.filter(status=1) # 在线新闻 qs = qs.filter(pub_time__year=2024) # 再叠加条件
2.3 all()
- 语义:拿到模型对应的 整张表。
- 返回值:QuerySet。
- 真正执行:迭代、切片、序列化、print 时才会发 SQL。
- 性能陷阱:数据量大的表慎用,必须加分页或切片。
2.4 exclude(**kwargs)
-
语义:反向过滤,等价于 SQL 的
NOT (条件)。 -
返回值:QuerySet。
-
与 filter 组合:
python# 排除已删除且是广告的文章 Article.objects.exclude(is_deleted=1).exclude(category='ad')
2.5 order_by(*fields)
- 语义:对 QuerySet 排序。
- 默认:升序(ASC)。
- 降序:在字段前加
-,如-read_count。 - 多字段:按书写顺序依次排序,
order_by('-top', 'id')。 - 与索引关系:确保排序字段有联合索引,避免 filesort。
2.6 QuerySet 三大特征(面试常深挖)
-
惰性(Lazy)
仅当"真正需要数据"时才发 SQL,例如:
pythonqs = User.objects.filter(level=3) # 0 次 SQL print(qs) # 1 次 SQL -
缓存(Cache)
同一个 QuerySet 第二次迭代会复用第一次的缓存,不会重复查库。
-
链式(Chainable)
每次 filter/exclude/order_by 都返回新的 QuerySet,可无限链下去,直到"触发执行"。
3. 章节总结
- get = "唯一行查询",找不到 or 找到多行都抛异常。
- filter/all/exclude 都返回 QuerySet,不抛异常;order_by 只是排序,依旧返回 QuerySet。
- QuerySet 是"惰性 + 可链 + 带缓存"的对象,真正 SQL 在"第一次使用数据"时才发出。
- 面试最爱问:get 与 filter 区别?QuerySet 何时发 SQL?order_by 如何降序?
4. 知识点补充
4.1 必须补充的 5 个高频考点
- values() / values_list():取指定字段,返回 dict / tuple 序列,减少序列化开销。
- select_related():一对一、多对一预加载,把 JOIN 提前做掉,解决 N+1。
- prefetch_related():多对多、反向一对多预加载,用 IN 查询拆成两条 SQL,同样解决 N+1。
- exists():比
len(qs)或bool(qs)更快,只做SELECT (1) AS "a" FROM ... LIMIT 1。 - only() / defer():按需取字段,减少 IO,与 values 区别是仍返回模型对象。
4.2 最佳实践(≥300 字)
线上接口永远别直接 Model.objects.all()。
第一步先加"分页":qs = qs[offset: offset+page_size],利用 QuerySet 切片惰性,只取需要行数。
第二步只拿必要字段:若仅返回 JSON,优先 values('id', 'title');若后续还要写实例方法,用 only('id', 'title') 把大文本字段 defer 掉。
第三步考虑预加载:当序列化需要外键表字段,如 article.author.name,就在视图里先 select_related('author'),把 JOIN 在一条 SQL 完成;如果是反向一对多 article.comment_set.all(),则用 prefetch_related('comment'),Django 会拆两条 IN 查询,比循环查库快 1~2 个量级。
第四步给排序字段建联合索引:例如 order_by('-publish_time', 'id'),就建 (publish_time DESC, id ASC) 的联合索引,避免 filesort 导致的临时表、磁盘排序。
最后监控 SQL:开启 DEBUG=True 时看 connection.queries,或者用 django-debug-toolbar、py-spy 采样,确认没有 N+1、没有全表扫描。遵循以上 5 步,可把最普通的列表接口 QPS 提升 3~10 倍,数据库 CPU 下降 50% 以上。
4.3 编程思想指导(≥300 字)
ORM 的本质是"把关系代数封装成对象语法",因此写代码时先想 SQL 再写 Python。
- 先画 ER 图,理清实体关系;再写 Django Model,把字段类型、db_index、unique_together 一次定义到位------这叫"模型即文档",让后续开发者一眼看懂业务。
- 查询时坚持"语义化":filter 里只写"业务含义",不写魔法数字。例如
status=2让人看不懂,封装成Article.ONLINE常量或QuerySet.online()方法,既可读又防错。 - 利用"链式接口"做"渐进式"构造:把公共过滤逻辑写成可复用的 QuerySet 方法(自定义 Manager),如
Article.objects.by_user(user).online().in_days(7),让视图层只关心组合,不关心细节。 - 牢记"懒加载"双刃剑:一方面避免过早发 SQL,另一方面要警惕"循环里不小心触发查询"。标准做法是:在视图层把需要的数据一次性取完,再传给模板或序列化器;禁止在模板/序列化器里再点外键。
- 最后,任何"性能优化"都要先度量后动手:用
count(*)、explain、慢日志确认瓶颈,再决定是加索引、改查询,还是拆表、拆库。ORM 让你更快迭代,但别让"看不见"的 SQL 拖垮整个系统------先思考关系,再写代码;先度量,再优化,这是资深工程师与新手最大区别。
5. 程序员面试题
-
简单
get()与filter()的核心区别?
get()用于获取唯一匹配 的模型实例:如果查询结果为空或多于一个,会分别抛出DoesNotExist或MultipleObjectsReturned异常。
filter()返回一个QuerySet,可包含零个、一个或多个对象,不会抛异常,适用于列表查询。 -
中等
如何把一个 QuerySet 按字段
created_time降序、再按id升序排列?pythonqs = qs.order_by('-created_time', 'id') -
中等
简述 QuerySet"惰性查询"的含义,并给出触发 SQL 的两种常见动作。
Django 的 QuerySet 是惰性的,即在定义或链式调用(如
filter()、exclude()、order_by())时不会立即执行数据库查询 ,只有在真正需要数据时才触发 SQL。常见触发动作包括:
- 对 QuerySet 进行迭代(如
for obj in qs) - 转为列表:
list(qs) - 调用
len()、bool()、print() - 使用
exists()、first()、last()等求值方法
- 对 QuerySet 进行迭代(如
-
困难
线上接口出现 N+1,如何定位并修复?请给出至少两种不同关系的解决代码。
定位方法:- 使用
django-debug-toolbar查看 SQL 执行次数; - 日志中观察重复的单条查询(如循环中频繁查外键)。
修复方案:
-
多对一(正向外键) :使用
select_related(单次 JOIN)pythonarticles = Article.objects.select_related('author').all() # 避免在循环中访问 article.author 触发额外查询 -
一对多(反向关系)或 多对多 :使用
prefetch_related(额外查询后内存关联)pythoncategories = Category.objects.prefetch_related('article_set').all() # 或 users = User.objects.prefetch_related('groups').all()
- 使用
-
困难
请写出一段代码,仅查询
User表中的id, username两列,并返回列表形式的字典,要求 SQL 只查一次且不带缓存。pythonresult = list(User.objects.values('id', 'username'))说明:
values()限制只查指定字段;list()强制立即执行 SQL(触发求值);- 默认不启用缓存,每次执行都会查库(除非手动使用
cache相关装饰器或中间件)。
Django ORM 条件查询完全指南
1. 章节介绍
本节聚焦 Django ORM 的"条件查询"语法,覆盖模糊匹配、比较运算、范围过滤、空值判断四大场景。掌握这些写法,可在不手写 SQL 的前提下完成 90 % 的业务过滤需求,也是后台接口、数据清洗、面试手写代码的高频考点。
| 知识点 | 面试频率 | 备注 |
|---|---|---|
| 双下划线语法(field__lookups) | 高 | 必背,Django 特有 |
| 模糊查询:contains/startswith/endswith | 高 | like 场景替代 |
| 范围查询:in / range | 中 | 多值与区间过滤 |
| 比较运算:gt / gte / lt / lte | 高 | 数字、日期通用 |
| 空值查询:isnull | 中 | 区别空字符串与 NULL |
2. 知识点详解
2.1 双下划线语法(field__lookups)
- 固定格式:
字段名__条件关键字=值 - 条件关键字大小写敏感,全部小写
- 链式过滤:
Model.objects.filter(tag__name__contains='科技')
2.2 模糊查询
| 关键字 | SQL 等效 | 示例 | 说明 |
|---|---|---|---|
| contains | LIKE '%科技%' | name__contains='科技' |
区分大小写;不区分用 icontains |
| startswith | LIKE '科技%' | name__startswith='科技' |
前缀匹配 |
| endswith | LIKE '%科技' | name__endswith='科技' |
后缀匹配 |
代码示例:
python
# 查询新闻类型名称里同时含"新"和"闻"
from news.models import NewsType
qs = NewsType.objects.filter(name__contains='新') \
.filter(name__contains='闻')
2.3 范围查询
- in:多值枚举
id__in=[1, 3, 5]→WHERE id IN (1,3,5) - range:闭区间
ctime__range=['2023-01-01', '2023-12-31']→BETWEEN
2.4 比较运算
| 关键字 | 含义 | 示例 |
|---|---|---|
| gt | > | age__gt=18 |
| gte | >= | score__gte=60 |
| lt | < | price__lt=100 |
| lte | <= | stock__lte=0 |
日期同样适用:
News.objects.filter(pub_time__gte=timezone.now()-timedelta(days=7)) # 最近 7 天
2.5 空值查询
field__isnull=True→IS NULLfield__isnull=False→IS NOT NULL
注意:Django 里空字符串
''与 NULL 是两种状态;仅当null=True才可能出现 NULL。
3. 章节总结
一张图背下所有写法:
filter(字段__关键字=值)
关键字:contains、startswith、endswith、
in、range、gt、gte、lt、lte、isnull
4. 知识点补充
4.1 必须补充的 5 个细节
icontains/istartswith/iendswith:不区分大小写版本。regex/iregex:直接写正则,适合复杂模式。date/year/month/day:按日期部分过滤,如pub_time__year=2023。exclude():取反过滤,等价于NOT。- 索引失效:前导模糊 (
LIKE '%xx') 会使 MySQL 索引失效,大数据量请用全文检索 (Elasticsearch、Whoosh)。
4.2 最佳实践(≥300 字)
场景 :千万级文章库,按标题模糊搜"科技"并只返回最近一年数据。
问题 :title__contains='科技' 在数据量大时全表扫描,接口 3 s+。
优化步骤:
-
模型层添加
db_index=True对pub_time建普通索引;title不建,因为LIKE '%科技%'用不到 B+Tree。 -
先用时间索引缩小范围,再模糊匹配:
pythonrecent = timezone.now() - timedelta(days=365) qs = (Article.objects .filter(pub_time__gte=recent) # 走索引,先剪 95 % 数据 .filter(title__icontains='科技')) # 仅扫描剩余 5 % -
列表接口必加分页:
qs[:20]或使用Paginator;避免一次性len(qs)。 -
模糊搜索升级:接入 Elasticsearch,Django 端用
django-elasticsearch-dsl,把title设为text+ik_max_word分词,查询改走search=Search().query('match', title='科技'),RTT < 100 ms。 -
缓存:热搜关键词每日预热,直接缓存
QuerySet的values_list('id', flat=True)20 k 条,命中缓存则回源 ID 二次查询,减少 80 % 数据库压力。
结论:条件查询语法简单,但大数据场景必须"先过滤高选择性字段,再模糊匹配",并配合索引、分页、缓存或搜索引擎,才能兼顾功能与性能。
4.3 编程思想指导(≥300 字)
-
把 ORM 当"编译器" :
任何
filter()都只是生成 SQL 的 AST,不要急于"优化得过早"。先用print(qs.query)观察真实 SQL,再用explain分析执行计划,最后决定加索引或改写法。 -
"关键字"思维模型 :
所有双下划线关键字本质是对 SQL 表达式的"命名封装"。记住映射关系即可举一反三:
gt→>、range→BETWEEN、isnull→IS NULL。面试手写代码时,先在草稿纸写出 SQL,再反向推 ORM,准确率提升 50 %。 -
组合优于继承 :
复杂查询用
Q()对象组合,而不是写长串if/else拼接。Q(title__contains='科技') | Q(tag__name='AI')可读性远高于字符串拼接,也能避免 SQL 注入。 -
"延迟求值"+"切片即触发" :
QuerySet只在真正需要数据时才发 SQL(迭代、切片、list()、len())。利用这一特性,在视图层先统一加select_related/prefetch_related做 N+1 防护,再返回给前端,减少 90 % 冗余查询。 -
测试即文档 :
为每个常用条件写单元测试,既防回归,又能在面试时直接引用 GitHub 链接作为"可运行简历"。示例:
python@pytest.mark.django_db def test_news_contains(): NewsType.objects.create(name='科技新闻') assert NewsType.objects.filter(name__contains='科技').exists()运行通过即证明语法正确,比口头背诵更可信。
5. 程序员面试题
-
简单
写出 ORM 查询"标题包含'Django'"的语句。
pythonModel.objects.filter(title__contains='Django') -
中等
查询
id在[10, 20, 30]且发布时间在过去 30 天内的文章。pythonfrom django.utils import timezone from datetime import timedelta Article.objects.filter( id__in=[10, 20, 30], pub_time__gte=timezone.now() - timedelta(days=30) ) -
中等
如何区分"字段为空字符串"与"字段为 NULL"?分别写出两种查询。
-
查询空字符串:
pythonModel.objects.filter(title='') -
查询
NULL值:pythonModel.objects.filter(title__isnull=True)
-
-
困难
当
title__contains='关键词'造成全表扫描时,你会如何优化?至少给出 3 种方案。- 缩小查询范围 :先通过时间、状态等高频过滤字段加索引筛选子集,再对子集做
contains; - 引入全文搜索引擎:如 Elasticsearch 或 Meilisearch,替代数据库模糊查询;
- 使用数据库原生全文索引 (如 MySQL 8.0 的 InnoDB 全文索引),配合
title__search='关键词'; - 缓存热门搜索词结果,降低数据库压力;
- 限制返回字段 ,使用
.values('id', 'title')减少网络与内存开销(虽不解决全表扫描,但提升整体性能)。
- 缩小查询范围 :先通过时间、状态等高频过滤字段加索引筛选子集,再对子集做
-
困难
用
Q对象实现:标题含"Python"或标签为"后端",且id大于 100。pythonfrom django.db.models import Q Article.objects.filter( (Q(title__contains='Python') | Q(tag='后端')) & Q(id__gt=100) )
Django 查询利器:F 对象与 Q 对象深度精讲
1. 章节介绍
在 Django ORM 中,F 对象 与 Q 对象是两大"高阶查询"核心工具:
- F 对象让"字段 vs 字段"的比较与运算在数据库层面完成,避免把数据拉到 Python 内存;
- Q 对象用"与 / 或 / 非"构造任意复杂逻辑,解决多条件组合、OR 查询、动态拼接等痛点。
| 核心知识点 | 面试频率 | 一句话定位 |
|---|---|---|
| F 对象原理与算术运算 | 高 | 字段级运算,减少内存与并发竞态 |
| Q 对象基础语法(& | ~) | 高 | 构造逻辑与或非,突破 filter 只能 AND 的限制 |
| 动态 Q 拼接 | 中 | 根据用户输入拼装复杂 where |
| F/Q 与 annotate/aggregate 联用 | 中 | 在聚合中继续使用 F/Q 完成跨表计算 |
| 性能陷阱与锁机制 | 低 | 避免 F 对象更新时的 Lost-Update |
2. 知识点详解
2.1 F 对象(django.db.models.F)
- 作用:把"字段"当变量直接放进 SQL 表达式,实现行内字段比较 与原子更新。
- 必须
from django.db.models import F后使用。 - 典型场景
- 字段自增、自减(阅读量+1,库存−1)。
- 字段间比较(找出阅读量 > 评论量 2 倍的文章)。
- 跨表比较(
F('author__level'))。
- 算术 / 位运算支持
+ - * / % **、| & ^、以及Func()表达式均可链式追加。 - 并发安全
update(stock=F('stock')-1)在数据库端单语句完成,避免"读-改-写"竞态。
代码示例
python
# 1. 查询阅读数恰好等于评论数的文章
News.objects.filter(read_num=F('comment_num'))
# 2. 阅读数 > 评论数*2
News.objects.filter(read_num__gt=F('comment_num') * 2)
# 3. 给所有文章阅读数 +1(原子)
News.objects.update(read_num=F('read_num') + 1)
2.2 Q 对象(django.db.models.Q)
- 作用:构造逻辑与或非 查询,突破
filter(a=1, b=2)只能 AND 的限制。 - 运算符
&逻辑与|逻辑或~逻辑非
- 基本范式
Model.objects.filter(Q(...) | Q(...)) - 动态拼接
利用reduce(operator.or_, q_list)快速把列表条件拼成"大 OR"。 - 与 filter/exclude 混用规则
- 一旦显式出现 Q,关键字参数 必须与 Q 用 & 连接,否则行为未定义。
- 空 Q 习惯
Q()返回恒真,可用于循环初始值。
代码示例
python
from django.db.models import Q
from operator import or_
from functools import reduce
# 1. 阅读>50 OR 评论>30
News.objects.filter(
Q(read_num__gt=50) | Q(comment_num__gt=30)
)
# 2. 动态 OR:用户输入标签列表
tags = ['python', 'django']
q_list = [Q(tags__name=t) for t in tags]
News.objects.filter(reduce(or_, q_list))
# 3. NOT:排除 id=1
News.objects.filter(~Q(id=1))
# 等价于
News.objects.exclude(id=1)
2.3 F + Q 联合实战
python
# 找出"阅读数 > 评论数*2" 且 "状态为已发布" 的文章
News.objects.filter(
Q(read_num__gt=F('comment_num') * 2) &
Q(status='published')
)
2.4 性能 & 事务注意
- F 对象更新默认使用隐式事务(单条 SQL)。
- 若业务要求"读-校验-写",需手动加
select_for_update()防止幻读。 - Q 对象过多会导致 SQL 括号层级加深,>100 个 OR 时建议改走全文检索或 ES。
3. 章节总结(速记)
- F 对象:把字段当变量,行内运算 、原子更新 、并发安全。
- Q 对象:用
& | ~构造任意布尔逻辑,解决 OR、动态拼接、NOT 查询。 - 二者可嵌套:
Q(read_num__gt=F('comment_num')*2)。 - 牢记:复杂 LIKE、全文搜索、>100 子句 OR 请交给搜索引擎,而非无限叠加 Q。
4. 知识点补充
Func()表达式:在 F 基础上调用数据库函数,如Upper(F('name'))。Case/When条件表达式:配合 F/Q 实现"if-else" SQL。Window窗口函数:F 对象可作为 partition_by / order_by 字段。select_for_update(skip_locked=True):高并发库存扣减标配。CheckConstraint:Django 2.2+ 支持用 F 对象在模型层加检查约束,保证库存 ≥ 0。
最佳实践(≥300 字)
高并发秒杀库存扣减是 F 对象最经典的应用场景。传统写法:
python
# 错误:并发 Lost-Update
goods = Goods.objects.get(id=1)
if goods.stock > 0:
goods.stock -= 1
goods.save()
在请求一多时会超卖。
正确:
python
from django.db.models import F
from django.db import transaction
with transaction.atomic():
rows = Goods.objects.filter(
id=1,
stock__gt=0
).update(stock=F('stock') - 1)
if rows == 0:
raise SoldOutException
要点
- 单条
UPDATE ... WHERE stock>0利用行级锁 与原子性解决竞态。 rows返回受影响行数,0 即库存已空,业务立即回滚。- 无需
select_for_update,减少一次 SELECT,提高 TPS。 - 若还需记录订单,把
insert into order放在同一事务,保证库存-订单一致。 - 对热点商品,可再叠加 Redis 预减库存 + MQ 异步落库 ,但数据库层仍用 F 对象做最终兜底,确保数据强一致。
编程思想指导(≥300 字)
-
"让数据库做它擅长的事"
字段比较、算术、布尔组合都是 SQL 的天然能力。把运算推向 DB,减少网络 IO 与 Python 内存,是性能优化的第一性原则。
-
"声明式优于命令式"
F/Q 对象让你**声明"要什么"**而非"怎么遍历"。代码从 10 行 for-loop 变成 1 行表达式,既简洁又利用底层索引。
-
"并发控制要靠近数据"
传统"读-改-写"在应用层加锁,距离数据远,锁粒度粗。F 对象的单语句更新把并发控制下沉到 InnoDB 行锁,粒度更细,冲突更少。
-
"动态条件用数据结构描述"
面对前端 20 个筛选项,不要写 20 个 if-else 拼 filter。统一转成
Dict[str, Any],再转Q(**dict)或reduce(or_, q_list),既易维护又避免 SQL 注入。 -
"认知复杂度 ≈ 括号深度"
当 Q 对象嵌套超过 3 层,或一个 filter 里出现 5 个以上 Q,立刻拆函数 。把"业务语义"封装成
build_user_q(params)、build_date_q(start, end),再组合,既单测友好又降低心智负担。
5. 程序员面试题
-
简单
如何用
F对象把文章阅读数原子地 +1?pythonfrom django.db.models import F Article.objects.update(read_num=F('read_num') + 1)该操作在数据库层面执行原子更新,避免竞态条件。
-
中等
写出"找出库存大于销量 2 倍且状态为在售"的 ORM 语句。
pythonfrom django.db.models import F Goods.objects.filter(stock__gt=F('sold') * 2, status='on_sale') -
中等
用户传入标签列表
['py', 'dj'],要求文章满足任一标签即返回,如何动态构造 ORM?pythonfrom functools import reduce from operator import or_ from django.db.models import Q tags = ['py', 'dj'] q = reduce(or_, (Q(tags__name=t) for t in tags)) Article.objects.filter(q)注意:若
tags为空,需额外处理避免reduce报错。 -
困难
高并发秒杀中,为何
goods.stock -= 1; goods.save()会超卖?给出基于F对象的解决方案并解释行锁流程。
问题原因:该写法是"读 → 修改 → 写"三步操作,非原子。多个并发请求可能同时读到相同的库存值(如 1),各自减 1 后保存,导致库存变为 -1,即超卖。解决方案(原子扣减 + 乐观锁):
pythonupdated = Goods.objects.filter(id=goods_id, stock__gt=0).update( stock=F('stock') - 1 ) if updated == 0: raise Exception("库存不足")行锁机制:
- 若
id是主键(或有唯一索引),InnoDB 会对该行加 排他锁(X Lock); UPDATE语句在判断stock__gt=0成立后直接在锁内完成减 1,确保整个判断+更新是原子的;- 其他并发请求必须等当前事务释放锁后才能继续,从而避免超卖。
- 若
-
困难
如何在不使用原生 SQL 的前提下,实现"id 不等于 1 且(阅读量 > 100 或评论量 > 50)"并保证最优索引?
ORM 写法:pythonfrom django.db.models import Q Article.objects.filter( ~Q(id=1), Q(read_num__gt=100) | Q(comment_num__gt=50) )索引优化建议:
- MySQL 5.7+ 支持 Index Merge Optimization ,可分别在
read_num和comment_num上建单列索引; - 更优方案是建立两个联合索引 :
(id, read_num)(id, comment_num)
利用id作为前缀加速NOT和OR条件的过滤;
- 若数据量极大,可考虑将条件拆分为两个
union()查询,分别走不同索引。
- MySQL 5.7+ 支持 Index Merge Optimization ,可分别在
Django ORM 聚合函数(aggregate)实战
1. 章节介绍
本节聚焦 Django ORM 的聚合函数,对应 SQL 中的 MAX / MIN / AVG / SUM / COUNT。通过 aggregate() 方法可在数据库层面一次性完成统计,避免把大量数据拉到 Python 内存再循环计算,显著提升性能并降低内存占用。掌握这些函数是编写数据报表、后台统计、接口聚合等功能的必备技能,也是面试高频考点。
| 核心知识点 | 面试频率 |
|---|---|
| aggregate() 基本语法 | 高 |
| Max / Min / Avg / Sum / Count 用法 | 高 |
| 聚合结果字典结构解析 | 中 |
| 多字段同时聚合 | 中 |
| 与 values() + annotate() 的区别 | 高 |
2. 知识点详解
2.1 聚合函数一览
Django 在 django.db.models 模块内置 5 大聚合类,首字母大写:
Max最大值Min最小值Avg平均值Sum求和Count计数(可去重distinct=True)
2.2 基本调用链
python
from django.db.models import Max, Min, Avg, Sum, Count
qs = News.objects.all() # 任意 QuerySet
result = qs.aggregate(Max('comment_count')) # 返回 dict
2.3 返回结构
aggregate() 永远返回一个字典,默认 key 为 字段名__聚合函数小写:
python
{'comment_count__max': 100}
可自定义 key:
python
qs.aggregate(max_comment=Max('comment_count'))
2.4 多字段一次性聚合
python
qs.aggregate(
max_c=Max('comment_count'),
min_c=Min('comment_count'),
avg_read=Avg('read_count'),
total=Sum('comment_count'),
news_count=Count('id')
)
2.5 空值处理
Avg自动忽略 NULL;Sum遇 NULL 当 0 处理Count('field')不计 NULL;Count('id')或Count('*')统计行数- 可通过
filter=Q(...)在聚合前先行过滤(Django 3.0+)
2.6 与 annotate() 区别
aggregate对整个 QuerySet 做整体统计,返回一个字典annotate先GROUP BY再对每组统计,返回每行附加聚合字段的 QuerySet
3. 章节总结
- 先
from django.db.models import Max, Min, Avg, Sum, Count - 在任意 QuerySet 后链
.aggregate(...),可一次传多个聚合类 - 结果是一个字典,key 可自定义;无数据时不会抛异常,值可能为
None - 统计行数优先用
Count('id');Count('*')同理 - 需要分组统计换用
values(...).annotate(...)
4. 知识点补充
4.1 补充知识
- 条件聚合:
aggregate(total=Sum('amount', filter=Q(status=1))) - 去重计数:
Count('author', distinct=True) - 原生 SQL 对比:
SELECT MAX(comment_count) FROM news; - 事务一致性:聚合与快照读在同一事务,可重复读隔离级别保证数据一致
- 性能优化:对聚合字段加索引可加速
MAX/MIN;COUNT(*)在 MySQL InnoDB 仍需行扫描,可借助冗余统计表或缓存
4.2 最佳实践(≥300 字)
线上报表接口经常需要「昨日新增 / 峰值 / 均值」等多指标一次性返回。若循环查询 N 次,既增加 RT 又占用连接池资源。最佳实践是:
- 只查一次数据库,用
aggregate一次性拿齐所有指标
python
from django.db.models import Sum, Max, Avg, Count
from django.utils import timezone
yesterday = timezone.now().date() - timedelta(days=1)
stats = News.objects.filter(
create_time__date=yesterday
).aggregate(
total_read=Sum('read_count'),
peak_read=Max('read_count'),
avg_read=Avg('read_count'),
news_cnt=Count('id')
)
- 对过滤字段
create_time建立联合索引,确保WHERE + AGG走覆盖索引 - 结果字典直接塞进 Redis 缓存,设置 TTL=1h,防止并发接口反复打数据库
- 若数据量达百万级,考虑物化视图或定时落库「日统计表」;接口直接查统计表,毫秒级返回
- 代码层做好
None兼容,如stats['avg_read'] or 0,前端展示更友好
通过「一次聚合 + 缓存 + 索引」三板斧,把统计接口 RT 从 800ms 降到 30ms,数据库 CPU 下降 45%,可支撑 10 倍流量增长。
4.3 编程思想指导(≥300 字)
聚合函数的核心思想是让计算靠近数据。数据库用 C/C++ 实现,跑在原生机器码层面,把 10 万条整型求和从 Python 循环的 O(n) 级解释器开销,转化为本地 SIMD 指令,性能提升可达两个数量级。作为工程师应牢记:
- 能一条 SQL 解决的,绝不拉回 Python for-loop;网络 IO + 解释器循环是性能杀手
- 聚合本质是 MapReduce 的局部 Reduce,先下推至存储引擎,大幅削减传输数据量
- 写代码前先画 ER 与索引图,确认聚合字段是否走索引;否则数据量上涨后接口必塌
- 聚合结果字典是「契约」,自定义 key 时要保持命名统一,如
total_amount、avg_amount,方便上游缓存与前端解析 - 单元测试里用
TransactionTestCase造空表、单条、多条三种场景,断言None / 0 / 正常值,防止空表抛 KeyError - 高并发场景记得加缓存,但缓存 Key 要带「业务维度 + 时间窗口」,避免脏读
- 养成查看
queryset.query与explain的习惯,确认聚合语句是否走最优索引;性能调优从 SQL 开始,而不是加机器
把「计算下推、索引先行、缓存兜底」三条思想固化到日常开发,可在需求方不断追加统计维度时,仍保持接口性能与代码可维护性的双赢。
5. 程序员面试题
简单题
Q: Django 中统计所有文章数量的最佳聚合语句是?
A:
python
from django.db.models import Count
Article.objects.aggregate(cnt=Count('id'))['cnt']
中等题
- Q: 如何一次性查询商品表中最高价格、最低价格及平均价格?
A:
python
from django.db.models import Max, Min, Avg
Product.objects.aggregate(
max_price=Max('price'),
min_price=Min('price'),
avg_price=Avg('price')
)
- Q:
aggregate与annotate有何区别?请给出代码示例。
A:
aggregate返回整个 QuerySet 的一个汇总字典;annotate先 GROUP BY 再对每行附加聚合值。
python
# 总销售额
Order.objects.aggregate(total=Sum('amount'))
# 每个用户的销售额
User.objects.annotate(total=Sum('order__amount'))
高难度题
- Q: 在百万级数据量的订单表中,如何快速计算昨日已完成订单的总金额与平均金额,并保证接口 RT < 50 ms?
A:
- 对
order表(create_time, status)建联合索引 - 使用
aggregate一次性统计:
python
Order.objects.filter(
create_time__gte=yesterday_start,
create_time__lt=today_start,
status='completed'
).aggregate(total=Sum('amount'), avg=Avg('amount'))
- 结果写入 Redis 缓存,Key 含日期与业务线,TTL 10 min
- 异步任务兜底,每 5 min 刷新缓存,防止缓存击穿
- 若仍慢,采用「日汇总表」+ 触发器实时更新,接口直接查汇总表
- Q: 某社交网站需要查询「每个用户发布的最大点赞数」及「全站最大点赞数」,要求一条 SQL 完成,如何写 Django ORM?
A:
python
from django.db.models import Max, OuterRef, Subquery
# 子查询:每个用户的最大点赞数
user_max = Post.objects.filter(
author=OuterRef('pk')
).values('author').annotate(max_like=Max('like_count')).values('max_like')
# 主查询:全站最大点赞数 + 每用户最大点赞数
max_dict = User.objects.annotate(
my_max=Subquery(user_max)
).aggregate(site_max=Max('my_max'))
利用 OuterRef + Subquery 先 GROUP BY 用户求 MAX(like_count),再对结果集求 MAX 得到全站峰值,全程一条复合 SQL,兼顾性能与正确性。
Django ORM 关联查询实战:从模型到后台完整链路
1. 章节介绍
本节以"图书-人物"一对多关系为例,完整演示 Django 中关联查询的前置准备:创建应用、编写模型、执行迁移、注册后台、构造测试数据,并解决后台外键下拉"对象显示不友好"的痛点。掌握这些步骤后,才能写出正确的正向/反向查询代码。
| 核心知识点 | 面试频率 |
|---|---|
| 一对多模型声明及外键选项(on_delete) | 高 |
| makemigrations & migrate 流程 | 中 |
| admin.site.register 与 ModelAdmin 自定义 | 中 |
__str__ 控制后台外键下拉可读性 |
高 |
| 正向/反向关联查询语法(未展开,本节打基础) | 高 |
2. 知识点详解
-
创建应用
python manage.py startapp box生成目录后,务必在INSTALLED_APPS列表中追加'box',否则迁移不会生成表。 -
声明一对多关系
python# box/models.py from django.db import models class Book(models.Model): title = models.CharField('书名', max_length=20) read = models.IntegerField('阅读量', default=0) comment= models.IntegerField('评论量', default=0) def __str__(self): return self.title # 关键点:让后台下拉显示书名 class Role(models.Model): name = models.CharField('人物', max_length=20) gender = models.SmallIntegerField('性别', choices=((1,'男'),(0,'女'))) book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='roles') # 反向别名 def __str__(self): return f'{self.name}({self.get_gender_display()})'on_delete=models.CASCADE必写,面试常问"外键删除策略"。related_name给反向查询起别名,如book.roles.all()。
-
迁移
bashpython manage.py makemigrations python manage.py migrate若提示 "no changes detected",99% 是忘记注册应用。
-
注册后台
python# box/admin.py from django.contrib import admin from .models import Book, Role @admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display = ['id','title','read','comment'] @admin.register(Role) class RoleAdmin(admin.ModelAdmin): list_display = ['id','name','gender','book']注册后才能在后台快速录入测试数据,验证关联是否生效。
-
录入数据技巧
-先在 Book 表新增 3 本书;
-新增 Role 时,外键下拉若显示"Book object(1)" 极不友好,解决方式就是给模型加
__str__,保存后刷新即可看到书名。
3. 章节总结
本节课完成了"图书-人物"一对多场景的数据层与后台层搭建:
- 建应用 → 2. 写模型(外键+on_delete+str ) → 3. 迁移 → 4. 注册 admin → 5. 造数据。
只有数据正确落地,下一节才能放心演示book.role_set.all()、Role.objects.filter(book__title__contains='天龙')等关联查询。
4. 知识点补充
- on_delete 其余选项:PROTECT、SET_NULL、SET_DEFAULT、DO_NOTHING 使用场景。
- db_index=True 对外键加索引,提升关联查询性能。
- select_related('book') 一次 SQL 把外键 JOIN 回来,避免 N+1。
- 多对多 ManyToManyField 与 through 自定义中间表。
- 后台 list_filter 按外键字段过滤,需用
('book', admin.RelatedOnlyFieldListFilter)。
最佳实践(>300 字)
生产环境写入测试数据时,不要手工在后台一条条点,而是编写"数据迁移脚本"或 Django-admin 命令,一方面可重复执行,另一方面能把初始数据纳入版本控制。步骤:
- 创建
box/management/commands/init_novel.py - 继承
BaseCommand,在handle()里用bulk_create批量插入 Book 与 Role; - 把脚本放在 CI/CD 流程,容器启动时自动
python manage.py init_novel; - 若数据量大,使用
iterator()分批读取,防止内存暴涨; - 对外键字段先插主表再插子表,保证主键已存在;
- 脚本幂等:先
get_or_create判断,防止重复执行导致唯一键冲突。
这样新同事一键即可拥有完整测试数据集,再也不用对着空白页面手动录入,极大提升开发效率。
编程思想指导(>300 字)
"先数据,后查询"是 ORM 学习的第一性原理。很多初学者急着写 .filter(),结果字段没加索引、外键忘写 on_delete、模型没有 __str__,导致后台无法调试、查询报错、性能爆炸。正确思路:
- 业务关系 → UML 草图 → 模型字段+关联类型;
- 每写一行模型代码,立刻问自己"以后我要怎么查?"------如果会按书名查人物,就在 Role 建外键并给 book.title 加索引;
- 把 Django Admin 当成"可视化单元测试",任何关联必须先能在后台顺利录入、展示,再写查询代码;
- 利用
related_name把"反向查询"语义化,让代码自解释:book.roles.all()比book.role_set.all()更易读; - 写完查询后一定打开
DEBUG=True的 SQL 面板,看是否产生 N+1,及时select_related/prefetch_related; - 模型即接口文档,保持字段命名与产品术语一致,减少前后端歧义。
坚持"数据层先行 + 可视化验证 + 性能观察"三步走,后续无论写 REST 接口还是 GraphQL,都能复用同一套模型思维,少走弯路。
5. 程序员面试题
简单题
- 写出在 Django 中声明"图书-人物"一对多模型的完整代码,并指出外键必须携带的参数。
答:见上models.py片段,必须写on_delete。
中等难度
-
后台外键下拉显示"Book object(1)"如何改成可读的"书名"?
答:给 Book 模型增加
__str__方法,返回self.title。 -
简述
makemigrations与migrate的区别。答:makemigrations 根据模型变更生成迁移文件;migrate 把迁移文件翻译成 SQL 并在数据库执行。
高难度
-
当删除一本图书时,若要求关联人物不被物理删除而是把外键置空,模型应如何写?
答:
book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True)。 -
已知需要按"图书阅读量降序取出每本书的前 3 个人物",请写出最高效的 ORM 查询,并说明如何避免 N+1。
答:
python
from django.db.models import Prefetch
books = Book.objects.order_by('-read').prefetch_related(
Prefetch('roles', queryset=Role.objects.order_by('id')[:3])
)
使用 prefetch_related + Prefetch 对象,一次性把每本书的 3 条人物数据查出来,避免循环内的额外 SQL。# [P10]框架基础:通过对象进行关联查询.ai-zh.srt 字幕整理结果
Django ORM 关联查询深度指南
1. 章节介绍
本章聚焦 Django ORM 中最常用的"关联查询":
给定已存在的外键/一对多关系,如何用最少的代码、最少的 SQL 把"一端"与"多端"数据一次性取出。
掌握两种入口:
- 从模型实例(对象)出发
- 从模型类(QuerySet)出发
| 核心知识点 | 面试频率 | 一句话速记 |
|---|---|---|
| 由一到多:obj.related_set.all() | 高 | 多端模型名小写 + _set |
| 由多到一:obj.foreign_key_field | 高 | 直接点外键字段 |
| 双下划线跨表查询(book__title) | 高 | 类名小写 + 双下划线 + 字段 |
| select_related / prefetch_related | 中 | 预加载,防 N+1 |
| related_name / related_query_name | 中 | 反向别名,代码可读性 |
2. 知识点详解
2.1 由一到多(正向→反向)
- 语法:
主对象.多端模型名小写_set.all() - 示例:
python
>>> from book.models import Book, Hero
>>> book = Book.objects.get(id=1) # 天龙八部
>>> book.hero_set.all() # 返回 QuerySet[Hero, ...]
-
底层:第一次访问时触发 SQL
sqlSELECT * FROM hero WHERE book_id = 1; -
常见错误:模型名没小写、漏
_set、误用filter(...)_set
2.2 由多到一(反向→正向)
- 语法:
多对象.外键字段 - 示例:
python
>>> hero = Hero.objects.get(id=7) # 韦小宝
>>> hero.book # 返回 Book 实例(鹿鼎记)
>>> hero.book.title # 继续点字段
- 底层:直接携带 JOIN 后的单条记录,无额外 SQL
2.3 跨表过滤(双下划线)
- 场景:不取对象,只按关联字段过滤
python
# 查询"出现郭靖"的所有书
Book.objects.filter(hero__name='郭靖')
# 查询"出版于2020年"的所有英雄
Hero.objects.filter(book__pub_date__year=2020)
- 面试陷阱:连续跨表最多三层,过深会被 DBA 打回
2.4 预加载:select_related & prefetch_related
select_related('foreign_key')
把一到一、多到一的外键 JOIN 进同一条 SQL,减少查询次数prefetch_related('反向_set')
先查主表,再一次性用 IN 查询多端,Python 端做拼接;适用于一对多、多对多- 代码对比:
python
# N+1 问题
books = Book.objects.all()
for b in books:
print(b.hero_set.all()) # 每本书一次 SQL
# 解决
books = Book.objects.prefetch_related('hero_set')
for b in books:
print(b.hero_set.all()) # 仅两条 SQL
2.5 反向别名 related_name
- 默认
hero_set可改名:
python
class Hero(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE,
related_name='heros')
# 使用
book.heros.all()
- 面试加分:若设置
related_name='+'会禁用反向关联,提高封装性
3. 章节总结
- 一到多:
obj.小写_set - 多到一:
obj.外键字段 - 过滤:
小写__字段双下划线 - 性能:
select_related(单端)、prefetch_related(多端) - 可读性:
related_name自定义反向名
4. 知识点补充 & 最佳实践
4.1 补充知识
QuerySet.select_for_update():行级锁,金融支付常用exists()比count()更快判断有无记录only()/defer()指定加载列,减少 IOthrough模型:自定义多对多中间表,可存额外字段(加入时间、角色)db_constraint=False:保留 ORM 查询,但不在数据库层建外键,适合遗留库
4.2 最佳实践(≥300字)
线上代码最常见性能事故是"N+1 查询"。
步骤一 :打开 DEBUG 工具栏或日志,确认 SQL 条数。
步骤二 :识别循环体内的关联访问;若循环外已知主键列表,优先用 __in= 一次查完。
步骤三:
- 若访问的是"单端"(如 Hero→Book),用
select_related('book') - 若访问的是"多端"(如 Book→Hero),用
prefetch_related('hero_set') - 若分页列表仅需计数,可
annotate(hero_cnt=Count('hero'))直接聚合,避免加载全部 Hero 对象
步骤四 :对超大表,联合 DBA 加复合索引;如hero(book_id, name)可加速book.heros.filter(name=)
步骤五 :写单元测试断言assertNumQueries(N),确保重构后 SQL 条数不膨胀。
遵循以上五步,可在不改动业务逻辑的前提下,把接口 120 条 SQL 降到 2~3 条,RT 从 2 s 降到 100 ms 内。
4.3 编程思想指导(≥300字)
ORM 把"关系"映射成"对象导航",但别忘了它终究生成 SQL。
先关系,后对象 :设计时先画 ER 图,确认实体、关联、基数,再写 Django 模型;避免把全部字段堆到一个模型,造成宽表。
显式优于隐式 :给每个 ForeignKey 写 related_name,让反向导航一眼能读;禁用默认 *_set 魔法。
延迟加载是双刃剑 :导航属性看起来是内存指针,实则可能触发 IO;在循环前先用 values() 或 select_related 把所需列一次性拉齐。
SQL 与 Python 边界清晰:
- 过滤、排序、聚合让数据库做(QuerySet 层面)
- 业务规则、格式转换让 Python 做(内存层面)
日志驱动开发 :开发阶段把django.db.backends日志开到 DEBUG,每写一行 ORM 代码就检查输出 SQL;形成肌肉记忆后,自然能预判 JOIN、索引、全表扫描。
可移植性思维 :避免手写原生 JOIN,即便 PostgreSQL 支持SELECT ... FOR UPDATE SKIP LOCKED,也要封装成 QuerySet 扩展,方便未来迁移到 MySQL 8。
把以上思想固化成团队规范,可在代码评审阶段自动过滤掉 80% 的潜在慢查询。
5. 程序员面试题
| 难度 | 题目 | 参考答案 |
|---|---|---|
| 简单 | 已知 hero.book 能拿到书实例,如何反向拿到一本书的所有英雄? |
book.hero_set.all() 或自定义 related_name='heros' 后 book.heros.all() |
| 中等 | 解释 select_related 与 prefetch_related 的区别与使用场景 |
select_related 做 SQL 级 JOIN,适用于单端外键;prefetch_related 分两条 SQL 查主表与多端,再用 Python 拼接,适用于一对多、多对多。错误混用会导致重复 JOIN 或内存爆炸 |
| 中等 | 写一条 QuerySet:查找"出版年份早于 2000 年且英雄姓名包含'乔'"的所有书籍 | Book.objects.filter(pub_date__year__lt=2000, hero__name__icontains='乔').distinct() |
| 困难 | 如何在不执行真正 SQL 的情况下,判断"某本书是否有英雄"? | Book.objects.filter(id=pk, hero__isnull=False).exists() 或先 annotate(c=Count('hero')) 再 filter(c__gt=0);利用 EXISTS 子查询,不拉取全表 |
| 困难 | 分页接口每页 20 本书,同时返回每本书的"英雄数量",请给出最优 ORM 代码并说明索引方案 | python<br>from django.db.models import Count<br>qs = Book.objects.only('id', 'title').annotate(hero_cnt=Count('hero')).order_by('-id')<br>page = qs[offset:offset+20]<br>
索引:在 hero(book_id) 建 B+Tree 索引;若经常按 book.id 倒序分页,可在 book(id DESC, ...) 建联合索引,覆盖 ORDER BY |# [P11]框架基础:通过模型类进行关联查询.ai-zh.srt 字幕整理结果
Django ORM 关联查询深度指南
1. 章节介绍
本节在"一对多"模型(Book → People)基础上,系统演示 Django ORM 的双向关联查询写法:
- 已知"一"查"多"------由书取全部人物;
- 已知"多"查"一"------由人物取所属书籍;
- 通过模型类 直接做跨表过滤,避免先查对象再导航的低效代码。
掌握这些写法,可一次性完成 SQL-Join 等价操作,是 Web 后端、数据脚本、面试手写 Query 的高频技能。
| 核心知识点 | 面试频率 |
|---|---|
| 正向查询:book.people_set.all() | 高 |
| 反向查询:people.book 或 people.book_id | 高 |
| 跨表过滤:__ 双下划线语法(关联模型小写__字段) | 高 |
| 双下划线支持的运算符(exact、contains、gt、in 等) | 中 |
| 同一查询中多跳跨表(如 people__book__author__name) | 中 |
| 防止 N+1:select_related / prefetch_related 选用场景 | 高 |
| 反向外键命名:related_name 与 _set 默认规则 | 中 |
| 聚合与分组:annotate + Count / Sum 结合关联表 | 低 |
2. 知识点详解
2.1 正向查询(一 → 多)
- 模型:
Book为"一",People为"多",People.book = ForeignKey(Book) - 语法:
book.people_set.all() - 自定义 related_name 后:
book.characters.all() - 过滤:
book.people_set.filter(age__gt=18)
2.2 反向查询(多 → 一)
- 直接访问外键字段:
people.book→ 返回 Book 实例 - 仅取外键值:
people.book_id(不会再次查库) - 修改外键:
people.book = new_book; people.save()
2.3 模型类跨表过滤(推荐写法)
- 语法模板:
ModelName.objects.filter(关联模型小写__字段__运算符=值) - 示例:
- 书→人:
Book.objects.filter(people__name__contains='宝') - 人→书:
People.objects.filter(book__title='天龙八部')
- 书→人:
- 支持多跳:
People.objects.filter(book__author__name='金庸')
2.4 常见运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
| exact | 精确等于 | book__id__exact=1 |
| contains | LIKE '%key%' | name__contains='风' |
| in | 范围 | id__in=[1,2,3] |
| gt/gte/lt/lte | 大小比较 | age__gt=20 |
| isnull | 空判断 | book__isnull=False |
2.5 性能优化
- 反向取"一"用
select_related('book')------ 单条 SQL 内连接 - 正向取"多"用
prefetch_related('people')------ 两条 SQL IN 查询 - 切忌 for 循环内部再发 SQL(N+1)
2.6 代码示例(完整可运行)
python
# 模型定义
class Book(models.Model):
title = models.CharField(max_length=100)
class People(models.Model):
name = models.CharField(max_length=50)
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='characters')
# 1. 已知书查人物(正向)
book = Book.objects.get(title='天龙八部')
qs = book.characters.all() # 不会立即执行
print(qs.values_list('name', flat=True))
# 2. 已知人物查书(反向)
people = People.objects.select_related('book').get(name='乔峰')
print(people.book.title) # 0 次额外查询
# 3. 跨表过滤(模型类级别)
# 查"人物名字含'宝'的书"
books = Book.objects.filter(characters__name__contains='宝')
print(books.values_list('title', flat=True))
# 4. 聚合:每本书的人物数量
from django.db.models import Count
book_list = Book.objects.annotate(cnt=Count('characters'))
for b in book_list:
print(b.title, b.cnt)
3. 章节总结
- 会用
__双下划线就能在 Django 里写任意方向的 Join。 - 正向查"多"用
xxx_set或related_name;反向查"一"直接取外键字段。 - 跨表过滤统一在
filter()里写关联小写__字段__运算符,避免先拿对象再遍历。 - 性能牢记
select_related(一对一/多对一)与prefetch_related(一对多/多对多)。
4. 知识点补充
4.1 额外 5 个高频扩展
- 自关联外键:如员工上下级
ForeignKey('self'),查询用__parent__name。 - 多对多:Through 模型自定义字段,查询方式与一对多完全一致。
- 反向命名冲突:同一模型被两个外键指向,需显式定义
related_name='a_set'/'b_set'。 - 跨表更新:
update(book=new_book)可一次性改多条,不会调用 save() 信号。 - 原生 SQL 对照:QuerySet 的
query属性可打印实际 SQL,方便面试手写验证。
4.2 最佳实践(≥300 字)
线上代码务必遵循"一次请求 ≤ 3 条 SQL"原则。
- 列表页:先用
prefetch_related把关联数据整体拉取,再用 Python 分组,杜绝循环查库。 - 详情页:用
select_related把外键对象一次性 Join 进来,模板里随意点属性都不会再发 SQL。 - 过滤条件动态拼接时,用
Q()对象组合,避免字符串拼接导致 SQL 注入。 - 当业务需要"外键+额外字段"唯一约束时,记得加
UniqueConstraint而不是靠代码判重,防止并发脏写。 - 大型项目把
related_name写成"动词+名词"形式,如authors_written,可读性远高于默认book_set。 - 最后,养成在单元测试里加
assertNumQueries(N)的习惯,任何重构都能立即发现 N+1 回退。
4.3 编程思想指导(≥300 字)
Django ORM 把关系代数 封装成对象导航 ,但底层仍是 SQL。思考时先画ER 图,分清"一"与"多",再决定方向:
- 写查询前,用"主表 → 条件 → 需要列 "三步法:主表是返回值类型,条件里放关联过滤,需要列决定
values()或only()。 - 遇到"先查 A 再循环查 B"的直觉,立刻提醒自己:能否用反向过滤 或聚合一次完成?
- 把
__双下划线当成"点"的 SQL 版本:每出现一次,就对应一次 JOIN;深度 >3 时要审视索引与必要性。 - 性能优化优先用集合思路:先拿所有 id,再一次性 IN 查询,最后 Python 字典映射,比逐条导航快 1~2 数量级。
- 面试手写 QuerySet 时,先写"伪 SQL"注释,再翻译为 ORM,既防漏条件,也便于面试官看懂你的 JOIN 逻辑。
5. 程序员面试题
| 难度 | 题目 | 参考答案 |
|---|---|---|
| 简单 | 已知书籍 id=1,用一行 ORM 取出所有人物姓名列表。 | People.objects.filter(book_id=1).values_list('name', flat=True) |
| 中等 | 查出"人物名字里含'红'且年龄大于 30"的书籍标题,要求不引入额外 SQL。 | Book.objects.filter(people__name__contains='红', people__age__gt=30).distinct().values_list('title', flat=True) |
| 中等 | 统计每本书的人物数量,并只保留数量 >5 的结果,按数量降序排列。 | Book.objects.annotate(c=Count('people')).filter(c__gt=5).order_by('-c') |
| 困难 | 手写 SQL 与等价 ORM:找出"没有人物的图书",要求使用 LEFT JOIN 且 ORM 部分只用一条 QuerySet。 | SQL: SELECT b.id FROM book b LEFT JOIN people p ON p.book_id=b.id WHERE p.id IS NULL; ORM: Book.objects.filter(people__isnull=True)| 困难 | 在并发场景下,如何保证"同一本书下人物名字唯一"?请给出模型层 + 数据库层双重方案,并写出对应的 Django 约束代码。 | 模型层:class Meta: constraints = [models.UniqueConstraint(fields=['book', 'name'], name='unique_person_name_per_book')];数据库层:迁移后自动生成唯一索引;并发插入时捕获 IntegrityError 并重试或提示用户。 |# [P12]框架基础:通过模型类进行数据增删查改.ai-zh.srt 字幕整理结果
Django ORM 增删改查(CRUD)实战
1. 章节介绍
本节以"书籍/人物"模型为例,演示如何完全脱离手写 SQL,仅通过 Django 模型类完成数据库的增、删、改、查操作。掌握这些模板代码后,即可在 90% 的业务场景中复用。
| 核心知识点 | 面试频率 | 备注 |
|---|---|---|
模型类实例化与 .create() |
高 | 批量插入、返回值 |
单条与批量删除 .delete() |
高 | 级联、软删除 |
单条更新与 .save() |
高 | 并发、只更新差异字段 |
| 查询集 QuerySet 基础 | 高 | 惰性求值、链式过滤 |
| F/Q 表达式与事务 | 中 | 原子性、竞态条件 |
| bulk_* 批量操作 | 中 | 性能对比 |
| select_related/prefetch_related | 低 | N+1 问题 |
2. 知识点详解
2.1 新增(Create)
-
单条
pythonbook = Book.objects.create(title="神雕", read_count=0, comment_count=0) # 返回值即是已保存的实例,自带 id -
批量
pythonBook.objects.bulk_create([ Book(title="倚天", read_count=0), Book(title="笑傲", read_count=0) ]) # 1 条 SQL,O(1) 次往返
2.2 删除(Delete)
-
单条
pythonpeople = People.objects.get(name="韦小宝") people.delete() # 返回 (deleted_count, deleted_dict) -
批量
pythonPeople.objects.filter(read_count=0).delete() -
软删除(推荐)
模型加
is_deleted = models.BooleanField(default=False)重写
delete()方法改为更新is_deleted=True,查询统一加.filter(is_deleted=False)
2.3 修改(Update)
-
单条先查后改
pythonp = People.objects.get(id=3) p.name = "扫地僧" p.save() # 会触发 pre/post_save 信号 -
一条 SQL 完成(避免竞态)
pythonPeople.objects.filter(id=3).update(name="扫地僧") -
差异更新(仅改动的字段)
pythonp.name = "扫地僧" p.save(update_fields=["name"]) # 生成 `UPDATE ... SET name=...`
2.4 查询(Read)
- 单条
get()不存在或多条都会抛异常,适合pk查询 - 多条
filter()、exclude()返回 QuerySet,可链式.order_by().values() - 惰性 & 缓存
QuerySet 真正迭代时才发 SQL;重复迭代会复用缓存 - 关联预取
select_related多对一/一对一,SQL 级联 JOIN;
prefetch_related多对多/反向一对多,Python 端 in 查询
3. 章节总结
Django ORM 把"表"映射成"类"、把"行"映射成"对象",通过 .create()、.delete()、.save()、.update() 四个入口即可覆盖全部日常 CRUD。牢记:
- 新增可
bulk_create - 删除推荐软删除
- 更新用
update_fields减少锁时间 - 查询用
select/prefetch_related防 N+1
4. 知识点补充
bulk_update:Django 2.2+ 提供,批量更新已存在对象,比逐条save()快 5~10 倍transaction.atomic:把多条 ORM 操作包成事务,要么全成功要么全回滚QuerySet.explain():打印 MySQL/PostgreSQL 执行计划,调优索引only/defer:只取部分字段,减少 IO;与values()区别是仍返回模型对象raw(): fallback 到原生 SQL,保持字段映射,适合复杂报表
最佳实践(≥300 字)
场景 :高并发给"阅读量"字段 +1
问题 :直接 book.read_count += 1; book.save() 会产生"丢失更新"
方案 :
a) 使用 F() 表达式,让数据库原子自增
python
from django.db.models import F
Book.objects.filter(id=book_id).update(read_count=F('read_count')+1)
b) 若业务还需模型信号或缓存失效,用 select_for_update 加行锁
python
with transaction.atomic():
book = Book.objects.select_for_update().get(id=book_id)
book.read_count += 1
book.save(update_fields=['read_count'])
c) 读多写极少且允许延迟,可用 Redis 做写缓冲,定时批量刷回 DB
结论:
- 计数器类更新优先
F(),零竞争、一条 SQL - 需要信号/业务钩子再用悲观锁
- 永远给
save()加update_fields,减少网络与日志,提高并发吞吐
编程思想指导(≥300 字)
- "表驱动"思维:把重复
if/else转换成数据+配置。例如状态机、权限位,用模型字段存码值,配合TextChoices枚举,代码即文档 - "资源即对象":Django 模型=业务实体,所有操作都通过方法封装进模型/管理器,如
Book.best_sellers()、People.soft_delete(),Controller 只负责调度,符合单一职责 - "SQL 先行":复杂查询先在数据库 CLI 跑通,再倒翻译成 ORM;用
QuerySet.query属性打印 SQL,确保命中索引 - "失败早暴露":利用 Django 校验层------
clean()、validators,在模型层就抛ValidationError,避免脏数据到 DB - "性能靠度量":每写一段数据访问,都加
assertNumQueries单元测试;生产环境开启connection.queries日志,>N 条即报警
坚持以上思想,ORM 不再是"黑盒",而是可预测、可单元测试、可水平扩展的业务基石
5. 程序员面试题
【简单】
- 写一条 Django ORM 语句,向 Book 表插入 title="三体" 的记录
答案 :Book.objects.create(title="三体")
【中等】
- 解释
select_related与prefetch_related的区别,并给出各自适用场景
答案 :select_related做 SQL JOIN,适用于多对一/一对一;prefetch_related做 Python 端二次查询+内存拼接,适用于多对多/反向一对多,可避免 N+1 - 如何防止"丢失更新"问题?至少给出两种 Django 实现
答案 :F()表达式原子更新select_for_update()行级锁- 在模型加
version = IntegerField(default=0)乐观锁,更新时version+1并检查 where 条件
【困难】
-
现有 100w 条 People 数据,需要把
read_count<10的批量改为 10,要求:- 内存占用 < 100 MB
- 总耗时 < 5 s
请给出完整代码与关键字段索引说明
答案:
python# 1. 给 read_count 加普通索引 # 2. 使用批量更新,避免全部载入内存 from django.db.models import F, Value, Case, When People.objects.filter(read_count__lt=10).update(read_count=10)仅一条 SQL,利用索引范围扫描,耗时≈1.2 s,内存≈0
-
设计一个"软删除"管理器,使得
People.objects.all()默认过滤掉已删除数据,但仍提供接口拿到全量数据
答案:pythonclass SoftDeleteManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_deleted=False) def all_with_deleted(self): return super().get_queryset() class People(models.Model): is_deleted = models.BooleanField(default=False) objects = SoftDeleteManager() all_objects = models.Manager() # 原始入口 def delete(self, *args, **kw): self.is_deleted = True self.save(update_fields=['is_deleted']) ```# [P13]框架基础:路由匹配使用入门.ai-zh.srt 字幕整理结果
Django 视图与路由分层管理实战
1. 章节介绍
本章在"Django 快速入门"基础上,用一个最小可运行的 news 应用,带你把"视图函数 → 路由分发 → 模板配置"完整走一遍。
重点解决"项目变大后路由全堆在 urls.py 里难以维护"的痛点,示范**多应用分层路由(include)**的规范做法,为后续 CBV、REST、中间件等进阶内容打地基。
| 核心知识点 | 面试频率 | 一句话速记 |
|---|---|---|
| 视图函数基本结构 | 高 | 必须返回 HttpResponse 或其子类 |
| include 实现路由分层 | 高 | 项目级 urls.py 只做分发,不做具体匹配 |
| 模板目录 DIRS 配置 | 中 | 一定加 os.path.join(BASE_DIR, 'templates') |
| 应用注册 & 迁移 | 中 | 三步:INSTALLED_APPS → makemigrations → migrate |
| 路由匹配顺序与"截断"规则 | 高 | include 后,剩余路径继续到下级 urls 匹配 |
| 正则匹配 vs path() | 低 | Django≥2.0 推荐 path,但老项目仍用 re_path |
2. 知识点详解
2.1 视图函数(FBV)最小骨架
python
# news/views.py
from django.http import HttpResponse
def index(request):
# request 是 WSGIRequest 对象,封装了本次 HTTP 所有信息
return HttpResponse('<h1>index 页面</h1>')
- 参数:第一个必须是
request,命名随意但约定俗成叫 request。 - 返回值:必须是
HttpResponse或其子类(JsonResponse、render 返回的 HttpResponse 等)。 - 命名:函数名就是路由里的"视图别名",也影响模板反向解析,保持见名知意。
2.2 路由分层(include 机制)
- 项目级 urls.py(只做"分发")
python
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('news/', include('news.urls')), # 关键行
]
- 应用级 news/urls.py(做"具体匹配")
python
from django.urls import path
from . import views
app_name = 'news' # 反向解析命名空间
urlpatterns = [
path('index/', views.index, name='index'),
path('login/', views.login, name='login'),
]
- 匹配过程
浏览器访问/news/index/→ 项目 urls 截掉news/→ 剩余index/交给news.urls→ 命中index/→ 执行views.index。
2.3 模板目录配置
python
# settings.py
import os
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 关键行
'APP_DIRS': True,
...
},
]
- 建议:项目根目录下建
templates/目录,全局模板放这里;各应用独有模板仍用app/templates/app/目录,防止重名冲突。 - 模板查找顺序:
DIRS→APP_DIRS=True各应用 templates 目录。
2.4 模型与迁移
python
# news/models.py
from django.db import models
class NewsInfo(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
def __str__(self):
return self.title
命令行:
bash
python manage.py makemigrations news
python manage.py migrate
- 新增模型后一定先 makemigrations 再 migrate,否则 ORM 无法同步到数据库。
__str__影响后台 admin 与调试时的可读性,建议所有模型都写。
3. 章节总结
- 视图函数 = 接收 request → 返回 HttpResponse。
- 路由分层 = 项目 urls 用 include 把"前缀"分发给各应用,应用 urls 再细化规则。
- 模板路径 = settings 里配置 DIRS,否则 render() 找不到全局模板。
- 新增应用三件套:注册 → 建模型 → 迁移。
- 访问链路:浏览器 → 项目 urls 截断 → 应用 urls 匹配 → 视图 → 响应。
4. 知识点补充
4.1 必会 5 个扩展点
- path 转换器:int:pk、slug:title 自动捕获并校验类型。
- re_path 正则:兼容旧版
(?P<name>\d+),适合复杂规则。 - app_name 命名空间:模板
{% url 'news:login' %}与 Pythonreverse('news:login')必备。 - 404/500 全局处理:在根 urls 加
handler404 = 'news.views.page_not_found'。 - 中间件顺序:request 自上而下,response 自下而上,自定义中间件一定测顺序。
4.2 最佳实践:多人协作中大型项目路由规范
当项目包含 20+ 应用、团队并行开发时,路由管理直接影响合并冲突与线上事故率。建议采用"三级路由 + 命名空间 + 版本前缀"方案:
-
一级路由(项目 urls)
只放"业务线前缀"与"版本前缀",如
path('api/v1/community/', include('community.urls'))禁止出现任何具体视图,确保后续可灰度升级 v2。
-
二级路由(应用 urls)
每个应用自建
urls/api.py、urls/admin.py、urls/mpa.py,把"接口/后台/前台"彻底隔离;统一加
app_name = 'community'命名空间,防止反向解析重名。 -
三级路由(子业务模块)
当应用再拆子模块时,可用
include()再次下发,例如
path('posts/', include('community.urls.posts'))。此时文件名即模块名,CRUD 路由一眼定位。
-
路由文档化
在应用根目录维护
ROUTING.md,用表格列出"URL → 视图 → 权限 → 备注",合并请求前强制更新,方便测试与运维 grep。 -
自动化检测
写单元测试遍历
urlpatterns,确保:- 无重复正则冲突(
re_path容易踩坑); - 所有命名路由均可
reverse(); - 线上 Sentry 报 404 时,能根据路由名快速定位代码文件。
- 无重复正则冲突(
采用上述规范后,我们团队 50+ 应用、累计 800+ 路由,一年内零路由冲突回滚,平均定位时间从 30 分钟降到 3 分钟。
4.3 编程思想指导:把"请求生命周期"刻进脑子
很多初学者写视图时只关注"函数里写什么",却忽略"请求怎么到我这里"。把生命周期画成一张图,随时反问"到哪一步了",能少踩 90% 的坑:
浏览器 → Web 服务器(Nginx)→ WSGI 容器(gunicorn/uwsgi)→ Django 中间件 → 路由匹配 → 视图 → 模板/序列化 → 中间件 → WSGI → Nginx → 浏览器。
- 定位 404:先看
include()是否截断正确,再看下级urlpatterns是否写错正则。 - 定位 500:先开
DEBUG=True看异常栈,确认是模型、SQL 还是序列化问题;再测中间件是否提前返回。 - 定位性能:用
django-debug-toolbar看 SQL 次数,用reverse()查路由是否重复匹配;把 Nginx 日志与 Django 日志对齐时间戳,确认慢在"网络"还是"SQL"。
养成"先画链路,再写代码"的习惯,你会自然写出高内聚、低耦合的视图:
- 视图只做"取数据 → 业务逻辑 → 返回"三件事,把权限、缓存、日志拆到中间件或装饰器;
- 路由只关心"路径→视图"映射,绝不写 SQL、调外部接口;
- 模板只关心"数据→HTML",绝不出现 ORM 查询。
当需求变更(如加版本、换权限体系)时,只需改动最小一层,其他层稳如磐石------这就是架构师追求的"可演进性"。
5. 程序员面试题
【简单】
- 写出 Django 视图函数的最小定义,并说明返回值类型。
答案:
python
from django.http import HttpResponse
def demo(request):
return HttpResponse('ok')
必须返回 HttpResponse 或其子类。
【中等】
-
解释
path('blog/', include('blog.urls'))的匹配与"截断"过程。
答案:访问
/blog/article/1/时,Django 先匹配blog/,把剩余article/1/截断后转发给blog.urls继续匹配;blog.urls里再写path('article/<int:pk>/', ...)即可命中。 -
反向解析时为什么要加
app_name?给出模板与 Python 两种写法。
答案:防止同名路由冲突。模板:
<a href="{% url 'blog:detail' pk=1 %}">;Python:reverse('blog:detail', kwargs={'pk': 1})。
【困难】
- 设计一个可灰度的多版本 API 路由方案,要求
/api/v1/与/api/v2/共存,且 v2 能复用 v1 大部分逻辑。
答案:
项目级 urls:
python
path('api/v1/', include('api.v1.urls', namespace='v1')),
path('api/v2/', include('api.v2.urls', namespace='v2')),
v2 视图继承 v1 视图,仅重写差异接口;利用 Django 视图继承或 DRF 的 versioning 类实现逻辑复用。
- 线上出现 30% 404 集中在
/news/detail/<int:pk>/,日志显示 pk 被传成字符串 "undefined",请给出排查与修复方案。
答案:
- 前端 JS 拼接 URL 时变量未定义,导致请求
/news/detail/undefined/; - 修复:前端用模板反向解析
{% url 'news:detail' pk=news.id %},或 JS 用URLTemplate替代字符串拼接; - 兜底:Django 侧加
re_path(r'^detail/(?P<pk>\d+)/$', ...)拒绝非数字,返回 410 并打日志,方便继续监控。# [P14]框架基础:路由的参数匹配.ai-zh.srt 字幕整理结果
Django 路由配置与参数提取深度总结
1. 章节介绍
本章围绕 Django 的 URL 路由系统展开,讲解请求从浏览器到视图函数的完整匹配流程,涵盖 path()/re_path() 用法、正则匹配、位置参数与命名参数提取等高频面试点。掌握本章可写出高可维护、高扩展的 URL 层,也是 Django 面试必考模块。
| 核心知识点 | 面试频率 |
|---|---|
| 路由匹配范围(不含域名端口) | 高 |
| path() vs re_path() 区别与场景 | 高 |
| 正则位置参数提取规则 | 高 |
| 命名组(?P)与视图形参一致性 | 高 |
| include() 实现多级路由 | 中 |
| 查询字符串 vs 路径参数 | 中 |
| 自定义 Path Converter(uuid、slug 等) | 低 |
2. 知识点详解
2.1 路由匹配范围
- Django 只拿 "路径部分" 做匹配(
/开始,到?或#前结束) - 协议 + 域名 + 端口由 Web 服务器(Nginx/uwsgi)处理,Django 不感知
2.2 根入口:ROOT_URLCONF
settings.ROOT_URLCONF = '项目.urls'
所有请求先进入该模块的urlpatterns列表,按顺序自上而下匹配,命中即停
2.3 path() 语法
python
from django.urls import path
urlpatterns = [
path('index/', views.index), # 严格匹配 /index/
path('users/<int:uid>/', views.user), # 捕获整数,变量 uid 传入视图
]
- 内置转换器:
strintsluguuidpath - 转换器失败 → 404
2.4 re_path() 正则匹配
python
from django.urls import re_path
urlpatterns = [
# 位置参数:/index/100 → views.index(request, '100')
re_path(r'^index/(\d+)/$', views.index),
# 命名参数:/user/100 → views.profile(request, uid='100')
re_path(r'^user/(?P<uid>\d+)/$', views.profile),
]
- 正则分组
()即为捕获;?P<name>显式命名 - 视图形参必须与命名组 完全一致 ,否则
TypeError
2.5 查询字符串(QueryDict)
/search?q=django&cat=video用request.GET.get('q')获取- 不属于路由层,不受 urlpatterns 匹配
2.6 include() 拆分路由
python
# 主 urls.py
urlpatterns = [
path('cms/', include('cms.urls')),
path('pay/', include('pay.urls')),
]
- 模块级解耦,大型项目必用
3. 章节总结
- 请求 → Web 服务器 → Django → ROOT_URLCONF → 顺序匹配 urlpatterns
- path() 适合静态、语义化 URL;re_path() 提供正则灵活性
- 捕获分组决定视图参数:位置参数按顺序,命名参数按名字;二者不可混用
- 查询字符串走
request.GET,与路由无关 - 多级路由用
include()拆分,保持 URL 与业务模块 1:1 映射
4. 知识点补充
4.1 补充知识
-
自定义 Path Converter
实现
class FourDigitConv: regex = r'\d{4}'并注册,可path('archive/<yyyy:year>/') -
URL 命名与反向解析
path('login/', views.login, name='signin')→{% url 'signin' %}或reverse('signin'),避免硬编码 -
app_name 空间
include 时指定
namespace='pay',模板{% url 'pay:checkout' %}防止不同 App 同名冲突 -
尾部斜杠策略
APPEND_SLASH=True自动 301 补/,SEO 更友好;REST API 常关闭 -
性能注意
urlpatterns 顺序影响性能,高频接口放前面;正则比转换器慢 3~5 倍,能用 path 就别 re_path
4.2 最佳实践(≥300 字)
"先语义、后性能、再兼容" 是设计 Django URL 的三段论。
- 语义化 :对人和搜索引擎友好。商品详情用
/product/<slug:slug>-<int:pk>/而非/p/123,既含关键词又保证主键唯一。 - 版本化 :对外 API 必须带版本,如
/api/v2/orders/。版本变动时旧链接仍然可用,通过include()把不同版本路由指向不同视图模块,实现 零侵入式升级。 - 参数最小化:只暴露必要变量,隐藏内部主键可用 Hashids 库把整数混淆成短字符串,防止爬虫遍历。
- 统一尾部斜杠 :团队约定全部带
/,Django 自动跳转,减少 404 日志。 - 自动化测试 :使用
django.test.Client针对每个 URL 模式写 resolve 与 reverse 双向测试,确保改名或重构后不会 404。 - 监控 :线上开启
django-request-logging,对匹配失败路径做聚类,及时发现黑客扫描或拼写错误导致的 404。
遵循以上 6 条,URL 层即可在长期迭代中保持 高可读、高稳定、高 SEO。
4.3 编程思想指导(≥300 字)
路由层是 "Web 系统的门牌号",其设计思想直接影响架构可扩展性。
- 开闭原则 :对新增开放、对修改关闭。用
include()给每个业务域独立urls.py,新增模块无需改动根路由文件。 - 单一职责 :一个视图只干一件事,URL 段也保持 "名词级" 粒度,如
/account/reset/、/account/profile/,拒绝把不同动作堆到同一条路由再用?action=区分。 - 显式优于隐式 :命名参数优先于位置参数,让调用方(模板、前端、API 网关)通过
reverse('pay:checkout', kwargs={'order_id': 123})一眼看清含义。 - 契约式编程 :路由正则就是 "对外契约" ,一旦上线不得随意收紧(否则老链接 404),版本号或自定义 Converter 给契约增加 "兼容缓冲区"。
- DRY :同一段前缀
/organization/<int:org_id>/出现 3 次以上即考虑封装成include()或自定义中间件注入org_id,避免重复捕获。
把上述思想固化为团队规范,代码即文档,新成员也能 "看 URL 就懂业务边界"。
5. 程序员面试题
| 难度 | 题目 | 参考答案 |
|---|---|---|
| 简单 | 写出一条 Django 路由,把 /book/2023/ 映射到 views.year_books(request, year),要求 year 只匹配 4 位数字 |
re_path(r'^book/(?P<year>\d{4})/$', views.year_books) |
| 中等 | 浏览器访问 /article/123/?page=2,如何在视图里分别拿到 123 和 2? |
article_id = kwargs['article_id'] # 来自路由 page = request.GET.get('page') # 来自查询字符串 |
| 中等 | 解释 path() 与 re_path() 在性能、可读性上的差异,并给出选型建议 | path 使用预编译转换器,性能高、可读性好,适合常规 RESTful 场景;re_path 支持复杂正则,性能略低,适合遗留兼容或特殊规则。优先 path,必要时 fallback 到 re_path。 |
| 困难 | 设计一个可扩展的多版本 API 路由方案,要求 v1 使用 /api/v1/,v2 使用 /api/v2/,且 v2 上线后 v1 不能下线,如何组织代码与路由? |
1. 目录隔离:api/v1/urls.py、api/v2/urls.py 分别实现各自视图; 2. 根路由:path('api/v1/', include('api.v1.urls', namespace='v1'))、path('api/v2/', include('api.v2.urls', namespace='v2')); 3. 视图层可继承,v2 复用 v1 逻辑并 override; 4. 测试层保持 reverse('v1:user') 与 reverse('v2:user') 独立; 5. 文档自动生成工具(drf-spectacular)按 namespace 区分,实现多版本并行维护。 |
| 困难 | 某老项目路由有大量 re_path(r'^xxx/(?P<id>\d+)$', ...) 未以 / 结尾,现在需要全局自动补 / 且不能改前端链接,给出零侵入实现思路 | 1. 中间件层拦截 request.path,发现不以 / 结尾且 request.method == 'GET';
-
构造新路径
new_path = path + '/',返回HttpResponsePermanentRedirect(new_path); -
在
settings.MIDDLEWARE最前插入该中间件,保证比 CommonMiddleware 先执行; -
单元测试覆盖带参、POST 请求,确保仅 301 一次;
-
灰度观察 Nginx 日志 302→301 变化,确认 SEO 权重无损。 |# [P15]框架基础:错误视图.ai-zh.srt 字幕整理结果
Django 自定义错误视图(404 / 500)实战
1. 章节介绍
本节课承接"路由参数匹配",聚焦线上友好错误页 的落地:
开发阶段 Django 会把异常栈直接吐给浏览器,方便调试;一旦上线,必须关闭 DEBUG 并给出不暴露源码 的 404/500 页面,否则既影响体验又带来安全隐患。课程手把手演示了"关 DEBUG → 放模板 → 生效"的完整闭环,并强调位置参数与关键字参数不要混用这一易踩坑点。
| 核心知识点 | 面试频率 |
|---|---|
| DEBUG=False 对错误页的影响 | 高 |
| 404.html / 500.html 放置规则与命名约定 | 高 |
| 模板查找顺序(TEMPLATE 配置) | 中 |
| 视图代码出错→Django 自动返回 500 机制 | 中 |
| 位置参数 vs 关键字参数混用风险 | 低 |
2. 知识点详解
-
关闭 DEBUG
settings.py中DEBUG = False- 必须同时给出
ALLOWED_HOSTS = ['*'](演示用)或真实域名,否则 Django 拒绝服务
-
404 触发时机
- URLconf 无匹配 → 404
- 模板/静态文件找不到不会触发 404,由 Web 服务器(Nginx)处理
-
500 触发时机
- 视图函数或中间件未捕获异常 → Django 捕获后返回 500
- 若再次渲染 500.html 又出错 → Django 退回到内置硬编码页面,不再泄露栈
-
模板放置
- 放在
TEMPLATES[0]['DIRS']指定的目录根下即可,文件名必须叫 404.html / 500.html - 生产环境推荐放在模板主题目录 便于复用,例如
templates/theme/error/404.html,再在视图里render(request, 'theme/error/404.html', status=404)
- 放在
-
参数混用警告
- 同一条路由里既写
<int:uid>又用path('user/<int:uid>/', views.detail, name='user-detail')没问题 - 但不要 前面用未命名组
(?P<uid>\d+)后面又用<int:uid>,会让正则与新版语法冲突,匹配失败率陡增
- 同一条路由里既写
3. 章节总结
- 上线三步曲:关 DEBUG → 配 ALLOWED_HOSTS → 放 404.html / 500.html
- 文件名固定,放在模板根目录即可生效;若想自定义路径,可在视图里手动
render并指定status - 视图代码抛异常 → Django 自动返回 500,无需手写 try/except
- 同一路由配置里避免混用"位置参数"与"关键字参数",降低正则冲突概率
4. 知识点补充
-
403、400 错误页
Django 同样支持
403.html/400.html,触发场景:CSRF 验证失败、SuspiciousOperation 等 -
Sentry 集成
生产环境建议接入 Sentry,异常日志自动上报,错误页仍对用户友好
-
Nginx 错误页兜底
即使 Django 未启动,Nginx 也能捕获 502/504,因此双层错误页(Nginx + Django)是标准做法
-
模板继承
404/500 页可继承
base.html,但必须保证父模板不依赖任何可能出错的上下文变量,否则 500 页会二次爆炸 -
单元测试
使用
django.test.SimpleTestCase.assertRaisesMessage与override_settings(DEBUG=False)可自动化验证错误页是否返回 200 且包含友好文案
最佳实践(≥300 字)
目标 :让错误页在任何极端情况下都能渲染成功 ,同时给运维留下足够排障信息。
步骤:
-
统一错误模板目录
templates/error/下放 400/403/404/500.html,全部继承同一套最小化error_base.html,仅保留品牌 Logo、一句话提示、返回首页按钮,不依赖任何模型查询 -
静态资源离线化
错误页用到的 CSS/图片全部内联 (base64 或
<style>标签),防止静态服务器挂掉时样式丢失 -
上下文安全降级
在
settings.py新建上下文处理器error_context,只返回{'STATIC_URL': '/static/'},杜绝使用可能抛异常的函数 -
日志双通道
- Django
logging配置把 500 异常写本地文件/var/log/django/error.log - 同时发 UDP 到 Sentry,保留完整栈供开发排查,但前端用户永远看不到
- Django
-
自动化回归
CI 阶段加一条:
python manage.py test --settings=settings_ci apps.core.tests.test_error_pages用
django.test.Client分别请求/ definitely_not_exist与人为抛出 RuntimeError 的视图,断言:- 状态码 404/500
- 响应体包含"返回首页"
- 响应体不包含
Traceback关键字
一旦测试失败即阻断上线
-
灰度开关
通过环境变量
FRIENDLY_ERROR_PAGE=on/off控制是否启用自定义模板,方便线上紧急回退
按以上六步落地,可保证用户体验、安全、排障效率三者兼得
编程思想指导(≥300 字)
"错误也是功能" ------把异常处理当成显性需求 写进 Backlog,而不是事后补丁。
防御式思维三层模型:
-
用户层
任何异常都要收敛到统一出口 :Django 的
handler404/handler500或 Spring 的@ControllerAdvice。禁止裸抛栈,文案统一、风格统一、跳转统一 -
服务层
业务代码只捕获可预期异常 (数据库唯一键冲突、第三方支付超时),记录后转译为用户可读错误码 ;不可预期异常 全部上抛,由全局兜底页处理。
规则:只有知道怎么恢复才捕获,否则让它炸
-
运维层
日志 = 现场照片,必须包含 request_id、user_id、SQL、堆栈、时间戳 ;同时用 APM(SkyWalking、Jaeger)做链路追踪,把"用户看到的错误"与"系统内部异常"一键关联
进阶:混沌工程 ------主动注入异常(kill 数据库连接、打满 CPU),验证错误页是否仍能渲染、告警是否及时。
最终形成**"异常→日志→告警→复盘→自动化用例"闭环,才能把"错误"从不可控变成可度量、可改进的稳定性功能**
5. 程序员面试题
简单题
- 问:Django 生产环境为什么要设置
DEBUG=False?
答:防止把异常栈、配置信息泄露给终端用户,同时激活自定义 404/500 错误页
中等难度题
-
问:访问不存在的 URL,Django 返回 404 的完整流程?
答:
- URLconf 无匹配 →
django.core.urlresolvers.resolve抛Resolver404 - Django 调用
django.views.defaults.page_not_found - 若存在
templates/404.html则渲染并返回 404 状态;否则返回内置硬编码页面
- URLconf 无匹配 →
-
问:500 错误页二次渲染失败会怎样?
答:Django 退回到内置硬编码 500 页面 ,不再尝试渲染任何模板,确保绝不再次泄露异常信息
高难度题
-
问:如何针对 AJAX 请求 返回 JSON 格式的 404/500,而非 HTML?
答:
- 在自定义
handler404/handler500视图里判断request.headers.get('x-requested-with') == 'XMLHttpRequest' - 返回
JsonResponse({'code': 404, 'msg': '资源不存在'}, status=404) - 同时保留
templates/404.html供普通页面请求降级使用
- 在自定义
-
问:高并发场景下,视图因数据库连接池耗尽 频繁 500,但错误页又依赖数据库(如读取站点配置表),如何保证错误页本身不崩 ?
答:
- 错误页视图禁止任何 ORM 查询,所有文案硬编码或读取内存级缓存(LocMemCache)
- 提前渲染 404/500 静态文件到
nginx/html/error/目录,由 Nginx 直接error_page 500 /error/500.html;兜底,彻底绕过 Django - 运维层加探针:连接池 < 10% 时即触发熔断,提前降级到静态页 ,实现双保险# [P16]框架基础:请求对象request.ai-zh.srt 字幕整理结果
Django HttpRequest 对象深度解析与实战
1. 章节介绍
本章围绕 Django 的 HttpRequest 对象 展开,讲解一次 HTTP 请求进入 Django 后,框架如何把浏览器发来的所有信息封装成 request 参数,以及如何在视图函数中读取路径、请求方法、查询参数、表单数据、上传文件、请求头、Cookie 等关键数据。掌握这些内容是编写交互式 Web 应用、定位请求问题、通过接口测试及面试考核的前提。
| 核心知识点 | 面试频率 | 一句话速览 |
|---|---|---|
| request 对象生命周期 | 中 | Django 在 URL 解析后、视图执行前实例化 HttpRequest |
| 常用属性(path、method、GET、POST 等) | 高 | 读取路径、方法、查询/表单参数 |
| QueryDict 与多值键 | 高 | request.GET/POST 是 QueryDict,支持一键多值 |
| GET 与 POST 区别与获取方式 | 高 | 查询参数 vs 请求体参数,分别用 request.GET / request.POST |
| 上传文件与 FILES | 中 | request.FILES 保存 multipart 上传 |
| 请求头与 META | 中 | 所有请求头放在 request.META,键全大写、加 HTTP_ 前缀 |
| Cookie 与 Session | 中 | request.COOKIES 读 Cookie,request.session 读写会话 |
| CSRF 校验流程 | 高 | Django 默认中间件校验 csrfmiddlewaretoken,可局部关闭 |
| 调试技巧(断点/print) | 低 | 断点或 print(request.__dict__) 快速查看属性 |
2. 知识点详解
2.1 request 对象从哪里来
- WSGI/ASGI 服务器把原始字节流解析成
environ字典 - Django 的
WSGIHandler用environ实例化django.core.handlers.wsgi.WSGIRequest,之后一直伴随视图生命周期 - 视图函数第一个参数必须是
request,名称可自定义但约定俗成
2.2 必会属性速查表
| 属性/方法 | 说明 | 代码示例 |
|---|---|---|
request.method |
大写字符串 "GET"、"POST"... |
if request.method == "POST": |
request.path |
只含路径,不含域名与查询串 | /article/123/ |
request.get_full_path() |
含查询串 | /article/123/?page=2 |
request.GET |
QueryDict,查询参数 | name = request.GET.get("name") |
request.POST |
QueryDict,表单体 | pwd = request.POST.get("pwd") |
request.FILES |
MultiValueDict,上传文件 | f = request.FILES["avatar"] |
request.body |
原始字节流,用于非表单 JSON | json.loads(request.body) |
request.META |
请求头+WSGI 变量 | ua = request.META.get("HTTP_USER_AGENT") |
request.COOKIES |
字典 | sid = request.COOKIES.get("sessionid") |
request.session |
类似字典,自动读写 Cookie | request.session["uid"] = 123 |
request.is_ajax() |
判断 X-Requested-With: XMLHttpRequest |
3.x 起用 request.headers.get("x-requested-with") |
request.is_secure() |
是否 HTTPS | --- |
request.user |
认证用户,未登录为 AnonymousUser |
if request.user.is_authenticated: |
2.3 QueryDict 与一键多值
- 继承自
MultiValueDict,内部用<key>: [<val1>, <val2>]存储 - 常用方法:
q.get("age")→ 取最后一个值,无则返回Noneq["age"]→ 同上,但 KeyErrorq.getlist("age")→ 返回列表,如["18", "19"]q.lists()→ 可迭代(key, [val, ...])
- 打印调试:
print(q.dict())取单值快照
2.4 获取 JSON / raw body
python
import json
def api(request):
if request.content_type == "application/json":
data = json.loads(request.body) # dict
注意:非表单时 request.POST 为空。
2.5 文件上传
- HTML 必须
enctype="multipart/form-data" - 视图读取:
python
file = request.FILES["avatar"] # InMemoryUploadedFile
dest = open("media/avatar.jpg", "wb+")
for chunk in file.chunks():
dest.write(chunk)
dest.close()
- 大文件用
file.chunks()避免一次性加载到内存
2.6 请求头读取规则
- 全部放在
request.META - 浏览器头小写+下划线+大写,加前缀
HTTP_,如:Accept-Language→HTTP_ACCEPT_LANGUAGEContent-Type→CONTENT_TYPE
- 自定义头建议加
X-前缀,避免与 WSGI 变量冲突
2.7 CSRF 校验与局部关闭
- 中间件
django.middleware.csrf.CsrfViewMiddleware默认启用 - 表单需加
{% csrf_token %}或前端拿 Cookie 手动带X-CSRFToken - 调试阶段可临时关闭:
python
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def login(request):
...
3. 章节总结
- Django 把一次 HTTP 请求封装成
HttpRequest实例,视图第一个参数即request - 查询参数用
request.GET,表单/体参数用request.POST,上传文件用request.FILES request.GET/POST是 QueryDict,支持一键多值,用.getlist()取列表- 请求头统一在
request.META,自定义头加HTTP_前缀 - 调试时打印
request.method / path / GET / POST / body可快速定位问题 - 面试常考 GET/POST 区别、CSRF 流程、文件上传步骤、QueryDict 与 dict 差异
4. 知识点补充
4.1 补充 5 个高频扩展点
HttpRequest.get_host():返回Host头,防 XSS 中做 Host 校验HttpRequest.build_absolute_uri(location):拼完整 URL,常用于分页、邮件request.content_type/request.encoding:解析非 JSON 或指定编码request.read(1024):底层流式读取,用于自定义协议转发request.META["REMOTE_ADDR"]取真实 IP,但注意反向代理需配置X-Forwarded-For
4.2 最佳实践:登录接口参数校验与日志(≥300 字)
实际开发中,登录接口往往同时支持 JSON POST 与 form POST,还要记录审计日志、防暴力破解。下面给出一份可直接落地的视图片段:
python
import json, hashlib, logging
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils.timezone import now
from ratelimit.decorators import ratelimit # 需 pip install django-ratelimit
logger = logging.getLogger("audit")
def get_client_ip(request):
xff = request.META.get("HTTP_X_FORWARDED_FOR")
return xff.split(",")[0] if xff else request.META.get("REMOTE_ADDR")
@ratelimit(key="ip", rate="5/m", block=True)
@csrf_exempt
def login_view(request):
# 1. 兼容 JSON / form
if request.content_type == "application/json":
data = json.loads(request.body)
else:
data = request.POST.dict()
username = data.get("username")
password = data.get("password")
# 2. 基础校验
if not (username and password):
return JsonResponse({"code": 400, "msg": "参数缺失"}, status=400)
# 3. 业务鉴权(伪代码)
if not check_password(username, password):
logger.warning("login_fail ip=%s user=%s", get_client_ip(request), username)
return JsonResponse({"code": 403, "msg": "用户名或密码错误"}, status=403)
# 4. 写 session & 日志
request.session["uid"] = username
request.session.set_expiry(3600)
logger.info("login_success ip=%s user=%s", get_client_ip(request), username)
return JsonResponse({"code": 0, "msg": "登录成功", "data": {"username": username}})
要点:
- 使用
@ratelimit限制单 IP 每分钟 5 次,防爆破 - 统一封装
get_client_ip,应对多层代理 - 日志格式固定,方便 ELK 后续分析
- 同时兼容
application/json与application/x-www-form-urlencoded,前端无需改动 - 登录成功后把
uid写入session,Django 自动下发sessionidCookie;后续视图用request.user或自定义中间件即可识别用户
4.3 编程思想指导:以"请求对象"为中心的设计(≥300 字)
- 面向封装 :把 Web 层所有输入收敛到
request对象,业务层只依赖"干净的数据结构"而非原始 HTTP,降低单元测试成本。 - 最小权限 :视图函数优先使用
request.POST.get()、request.GET.getlist()而不是直接读request.body,避免解析副作用;对敏感接口统一加@csrf_exempt或自定义鉴权中间件,保持"默认安全"。 - 契约优先 :团队内部约定"查询用 GET、提交用 POST、REST 更新用 PUT/PATCH、删除用 DELETE",并在代码里用
request.method做断言,拒绝非法动词,降低调试难度。 - 流式与内存平衡 :小文件直接
request.FILES["f"].read(),大文件必须chunks();JSON 小于 1 MB 可直接request.body,大于 1 MB 考虑客户端分片或直传 OSS,避免占用 Python 进程内存。 - 可观测 :所有视图入口打印
request.get_full_path()与request.content_type,并记录响应时间;线上出错时结合 Sentry 的 "Request" 面板可秒级还原参数、请求头、Cookie,极大缩短排障时间。
把以上思想固化到脚手架:新建项目即带统一异常处理、IP 获取、日志格式、限流中间件,后续业务开发者只需关注"从 request 中取业务参数",就能写出高可测、高可维护、面试能讲清原理的 Django 代码。
5. 程序员面试题
简单题
- 在 Django 视图里如何取得查询参数
page的值?请写出两种写法。
答案
python
page = request.GET.get("page") # 方式1
page = request.GET["page"] if "page" in request.GET else None # 方式2
中等难度
- 当 HTML 表单存在同名多选框
<input type="checkbox" name="tag" value="python">,后端如何拿到全部已选项?
答案
python
tags = request.POST.getlist("tag") # 返回列表,如 ["python", "django"]
- 请解释
request.POST与request.body的区别,并说明在什么情况下request.POST为空。
答案
request.POST只封装application/x-www-form-urlencoded或multipart/form-data的解析结果request.body是原始字节流,任何请求方式都有值- 当
Content-Type为application/json或text/plain时,Django 不会解析表单,故request.POST为空
高难度
-
线上环境通过 Nginx → Gunicorn → Django 获取客户端真实 IP 时,为什么有时
request.META["REMOTE_ADDR"]得到的是 127.0.0.1?如何正确获取?
答案
Nginx 作为反向代理,与 Gunicorn 本地回环通信,WSGI 变量REMOTE_ADDR被设为 Nginx 本地地址。应在 Nginx 配置proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
并在 Django 设置 USE_X_FORWARDED_FOR = True 并启用 RemoteAddrMiddleware(或自定义 get_client_ip 方法优先读 HTTP_X_FORWARDED_FOR),才能拿到真实 IP。
- 实现一个装饰器,要求:
- 只允许 AJAX 请求访问(
X-Requested-With: XMLHttpRequest) - 只接受
application/json格式 - 失败时返回 406 Not Acceptable
请给出完整代码。
答案
- 只允许 AJAX 请求访问(
python
from functools import wraps
from django.http import JsonResponse
def ajax_json_only(view_func):
@wraps(view_func)
def _wrapped(request, *args, **kwargs):
if request.headers.get("x-requested-with") != "XMLHttpRequest":
return JsonResponse({"msg": "AJAX only"}, status=406)
if request.content_type != "application/json":
return JsonResponse({"msg": "Content-Type must be application/json"}, status=406)
return view_func(request, *args, **kwargs)
return _wrapped
# 使用
@ajax_json_only
def api_demo(request):
data = json.loads(request.body)
return JsonResponse({"echo": data})
```# [P17]框架基础:响应对象Respose.ai-zh.srt 字幕整理结果
# Django HttpResponse 对象深度解析与实战
## 1. 章节介绍
本节课围绕 Django 视图层最核心的返回单元------HttpResponse 对象展开。课程从"为什么视图必须返回 HttpResponse"切入,结合源码讲解 render 的本质,随后通过 4 组现场编码演示:手动构造响应、修改状态码、设置/读取/删除 Cookie,让学员彻底掌握"视图函数→响应对象→浏览器"这一数据链路的底层细节。最后给出 Cookie 在登录态、埋点、推荐系统等生产场景的最佳实践,为后续中间件、缓存、DRF 响应器打下坚实基础。
| 核心知识点 | 面试频率 | 一句话速记 |
|------------|----------|------------|
| 视图必须返回 HttpResponse 对象 | 高 | 不返回就抛 `ValueError` |
| render 本质是 TemplateResponse → HttpResponse | 中 | 源码里先渲染再 return |
| 状态码 200/301/404/500 手动指定 | 高 | `status=404` 即可 |
| Content-Type、charset 参数 | 中 | 默认 `text/html; charset=utf-8` |
| Cookie 增删改查 & 过期策略 | 高 | `set_cookie` / `delete_cookie` |
| Cookie vs Session 区别与选型 | 高 | Cookie 存客户端,Session 存服务端 |
| SameSite、HttpOnly、Secure 安全属性 | 中 | 防 XSS & CSRF 必备 |
| 浏览器携带 Cookie 机制 | 中 | 每次请求自动带 `Cookie` 请求头 |
---
## 2. 知识点详解
### 2.1 视图返回值规范
- Django 的 `django.http.HttpResponseBase` 是所有响应类的基类
- 视图函数若返回非 HttpResponse 对象(如字符串、字典),Django 会抛出 `ValueError: The view didn't return an HttpResponse object`
- `render()` 快捷函数内部先加载模板→渲染字符串→封装成 `HttpResponse` 再返回,因此同样满足规范
### 2.2 HttpResponse 构造参数
```python
from django.http import HttpResponse
response = HttpResponse(
content='Hello<br>world', # 1. 内容(str/bytes/iterable)
content_type='text/html', # 2. MIME 类型,默认 text/html
status=200, # 3. 状态码,默认 200
charset='utf-8' # 4. 编码,默认 settings.DEFAULT_CHARSET
)
- 生产环境推荐显式指定
content_type='application/json'防止 XSS - 下载文件时设置
content_type='application/octet-stream'并附加Content-Disposition头
2.3 状态码场景速查
| 码 | 场景 | 搜索引擎友好 |
|---|---|---|
| 200 | 正常渲染 | ✅ |
| 301 | 永久重定向 | ✅ |
| 302 | 临时重定向 | ✅ |
| 404 | 资源不存在 | ✅ |
| 500 | 服务器异常 | ❌(被收录后影响排名) |
2.4 Cookie 全生命周期管理
- 写入:
response.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite=None)max_age秒级有效期;expires是datetime对象,二者选其一- 不指定时=会话 Cookie,关闭浏览器即失效
- 读取:
request.COOKIES.get('token') - 删除:
response.delete_cookie(key, path='/', domain=None)
注意:删除时必须与写入时的path/domain完全一致,否则浏览器忽略
2.5 安全加固
httponly=True禁止 JS 读取,防 XSSsecure=True仅 HTTPS 传输samesite='Strict'彻底阻止 CSRF POST 请求携带 Cookie;'Lax'允许顶级导航 GET- 敏感数据不要直接放 Cookie,可存
sessionid或JWT签名串
3. 章节总结
视图必须返回 HttpResponse;render 只是帮你套了模板。掌握 status、content_type、set_cookie/delete_cookie 就能自由控制"返回什么、怎么存、如何删"。Cookie 是前端状态的第一站,合理设置安全属性才能抵御 XSS & CSRF。
4. 知识点补充
4.1 额外 5 个高频考点
JsonResponse继承自HttpResponse,自动序列化字典+设置content_type=application/jsonStreamingHttpResponse用于大文件流式下载,节省内存FileResponse基于StreamingHttpResponse,封装了Content-Disposition: attachmentHttpResponseRedirect与redirect()快捷函数,内部状态码 302,可改permanent=True→301set_signed_cookie(key, value, salt='')带签名防篡改,读取用get_signed_cookie
4.2 最佳实践:登录态"双 Token" 方案(Cookie + Redis)
背景:纯 Cookie 存 JWT 易被 XSS 窃取,纯 Session 又难做分布式。
方案:
- 登录成功后,服务端生成两个串:
access_token:JWT,有效期 15 min,写入HttpOnly/Secure/SameSite=StrictCookierefresh_token:随机 32 位字符串,有效期 7 天,写入SecureCookie(HttpOnly=true)
- 将
refresh_token作为 key,用户 ID、权限、过期时间序列化后存入 Redis,并设置 7 天 TTL - 每次请求带
access_token,网关层直接验签,无需查 Redis,性能高 - 若
access_token过期,浏览器调用/api/token/refresh,服务端校验refresh_token是否在 Redis 中:- 存在 → 颁发新
access_token并更新 Redis TTL - 不存在 → 返回 401,前端跳转登录
- 存在 → 颁发新
- 登出时,后端
delete_cookie双 Token,同时DEL refresh_token清 Redis;即使攻击者窃取了本地 Cookie,也无法在服务端换到新 Token
优点:
- 验签无 I/O,QPS 高
- 泄露后可通过 Redis 一键吊销
- 兼容 SSR 场景,Cookie 自动携带,前端零感知
4.3 编程思想指导:把"响应"当成"消息"
- 单一职责:视图函数只负责"业务→消息",渲染或序列化交给专用组件(模板、序列化器)
- 不可变消息:一旦
HttpResponse对象 return,就不可再修改;需要中间件层级加工时,使用response.setdefault()而非直接赋值 - 契约优先:先写
content_type&status,再填content;对外接口文档即代码 - 最小暴露:Cookie 只存"指针"(sessionid / token),不存实体数据,减少网络往返与隐私风险
- 失败快速:异常分支尽早
return HttpResponse(status=4xx),避免深层嵌套 if-else,提高可读性
5. 程序员面试题
【简单】
- Django 视图函数能否直接
return "hello"?为什么?
答:不能,必须返回HttpResponse或其子类,否则抛ValueError
【中等】
- 如何让浏览器关闭后 Cookie 自动失效?
答:调用set_cookie时不传max_age也不传expires,生成会话 Cookie - 前端 JS 无法读取 Cookie,可能后端做了哪些设置?
答:httponly=True;也可能是secure=True且当前协议为 HTTP;或samesite=Strict导致跨站不携带
【困难】
- 描述一次"登录→颁发 Token→后续请求鉴权"的完整 Cookie 交互流程,并指出如何防止 CSRF
答:登录后服务端set_cookie(key='token', value=jwt, httponly=True, secure=True, samesite=Strict);后续浏览器同一站点请求自动带 Cookie;服务端在中间件校验 JWT。CSRF 防护:Strict 禁止跨站 POST 携带 Cookie,或配合X-CSRFToken双重验证 - 现网出现用户偶尔"登录态丢失",access_token 未过期但 refresh_token 仍在,可能原因?
答:a) 负载均衡节点时钟不一致,JWT 验签认为过期;b) Cookie path/domain 不一致导致 refresh_token 未上传;c) Redis 内存淘汰策略把 refresh_token 提前删除;d) 用户手动清除 Cookie 仅清除了 access_token# [P18]框架基础:路由重定向.ai-zh.srt 字幕整理结果
Django JsonResponse 与重定向实战
1. 章节介绍
在前后端分离已成主流的今天,Django 后端几乎不再直接渲染 HTML,而是把数据以 JSON 形式吐出,再由前端框架消费;同时,业务中经常需要"处理完就跳转"。本章用 10 分钟讲透两条高频技能:
- 用 JsonResponse 优雅返回 JSON
- 用 HttpResponseRedirect / redirect() 做重定向
| 核心知识点 | 面试频率 | 备注 |
|---|---|---|
| JsonResponse 原理与用法 | 高 | 必问,手写代码 |
| 返回非 dict 数据时的 safe 参数 | 高 | 易踩坑 |
| Content-Type 与 charset 细节 | 中 | 定位乱码 |
| 重定向 301 vs 302 | 中 | 搜索引擎优化场景 |
| redirect() 快捷函数源码 | 低 | 进阶加分 |
2. 知识点详解
2.1 JsonResponse 本质
- 位于
django.http.JsonResponse,继承自HttpResponse - 构造函数把 Python 对象 → JSON 字符串 → 字节流,并强制
Content-Type: application/json - 关键源码(简化)
python
class JsonResponse(HttpResponse):
def __init__(self, data, encoder=DjangoJSONEncoder,
safe=True, json_dumps_params=None, **kwargs):
if safe and isinstance(data, (list, tuple)):
raise TypeError('safe=False 才能序列化 list')
json_str = json.dumps(data, cls=encoder, **(json_dumps_params or {}))
kwargs.setdefault('content_type', 'application/json')
super().__init__(content=json_str, **kwargs)
2.2 常用 4 种写法
- 直接返回字典
python
return JsonResponse({'code': 0, 'msg': 'ok'})
- 返回列表,记得 safe=False
python
return JsonResponse([1, 2, 3], safe=False)
- 自定义日期格式
python
class CJsonEncoder(DjangoJSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.strftime('%Y-%m-%d %H:%M:%S')
return super().default(o)
return JsonResponse(data, encoder=CJsonEncoder)
- 中文不乱码:ensure_ascii=False
python
return JsonResponse(res, json_dumps_params={'ensure_ascii': False})
2.3 重定向两种实现
| 方式 | 状态码 | 场景 | 示例 |
|---|---|---|---|
| HttpResponseRedirect | 302 | 通用 | return HttpResponseRedirect('/index/') |
| redirect() 快捷函数 | 302/301 | 推荐 | return redirect('order:detail', order_id=123) 支持 URL 别名、模型 |
- 301 永久重定向:搜索引擎权重转移
- 302 临时重定向:登录后跳回原页面
- 内部重定向只发一次请求,浏览器地址栏会变
3. 章节总结
JsonResponse = HttpResponse + JSON 序列化 + 正确 Content-Type,注意 safe=False 与 ensure_ascii;
重定向用 redirect() 最简洁,牢记 301/302 语义差异即可。
4. 知识点补充
- Django 的
DjangoJSONEncoder可序列化 Decimal / UUID / datetime - 当数据量 > 1 MB 时,建议开启 GZip 压缩:
Content-Encoding: gzip - 重定向路径以 / 开头会跳过 URLconf,直接按绝对路径解析
- 若需返回 201 Created,可继承 JsonResponse 并指定 status=201
- 前后端分离项目务必开启 CORS 中间件,否则浏览器会拦截 JSON 响应
最佳实践(≥300 字)
生产环境返回统一外层格式,前后端契约清晰,日志可追踪:
python
# utils/response.py
from django.http import JsonResponse
def ok(data=None, *, msg='success', code=0, status=200, **kwargs):
"""标准成功响应"""
return JsonResponse({'code': code, 'msg': msg, 'data': data},
status=status, **kwargs)
def fail(msg='error', *, code=1, status=400, **kwargs):
"""标准失败响应"""
return JsonResponse({'code': code, 'msg': msg, 'data': None},
status=status, **kwargs)
# views/order.py
from utils.response import ok, fail
def create_order(request):
try:
order = do_create(request.POST) # 可能抛校验异常
except ValueError as e:
return fail(str(e)) # 400
return ok({'order_id': order.id}) # 200
优点:
- 统一字段名,前端拦截器可根据 code 做全局提示
- 集中处理中文编码、日期格式、浮点精度
- 后期加签名、加密、分页字段,只需改一处
- 单元测试可直接断言
resp.json()['code']
编程思想指导(≥300 字)
"返回 JSON"看似只是调个函数,背后体现的是契约思想 与分层思想。
- 契约优先:前后端先约定外层格式、字段含义、错误码区间,再写代码;契约一旦发布,向后兼容,杜绝"顺手改个字段名"的破坏行为。
- 分层隔离:视图层只负责"取参→调业务→拼数据→返回",不把 SQL 或第三方 SDK 异常直接抛给前端;通过标准响应包装,让前端拿到的是结构化错误,而非 500 堆栈。
- 最小惊讶:JsonResponse 默认 safe=True,防止开发者无意返回数组造成 JSON 劫持;理解框架设计意图,比死记硬背参数更重要。
- 性能与可维护权衡:小对象直接序列化,大列表(如导出 10 万行)应改用 StreamingHttpResponse 或分页,避免一次性吃掉内存。
- 测试即文档:给每个接口写 doctest / pytest,断言返回结构,比事后补 Swagger 更准确。
把"返回数据"当成对外发布的 API 合约,而不是临时补丁,你的代码将天然具备可扩展、可迁移、可回滚的能力。
5. 程序员面试题
【简单】
- JsonResponse 与 HttpResponse 在返回 JSON 时的核心区别是什么?
答:JsonResponse 会自动把 Python 对象序列化成 JSON 字符串,并设置Content-Type: application/json;HttpResponse 需要手动序列化并设置头。
【中等】
- 返回列表
[1,2,3]时浏览器报 TypeError,如何解决?
答:JsonResponse 默认 safe=True,只允许 dict,把safe=False即可。 - 登录成功后跳转到个人中心,请写出两种重定向代码。
答:
python
# 方法1
return HttpResponseRedirect('/user/profile/')
# 方法2(推荐)
return redirect('user:profile') # 依赖 URL 别名
【困难】
- 构造一个返回 201 且带 Location 头的 JSON 接口,要求手写视图。
答:
python
from django.http import JsonResponse
def create_book(request):
book = Book.objects.create(**request.POST.dict())
resp = JsonResponse({'id': book.pk}, status=201)
resp['Location'] = f'/api/books/{book.pk}/'
return resp
- 如何防止大型列表序列化导致内存暴涨?给出 Django 端实现思路。
答:使用生成器 + StreamingHttpResponse 分段序列化,或采用 Django REST framework 的 Pagination 类,限制每页大小;亦可将任务丢给 Celery,异步生成文件后重定向到下载地址。# [P19]框架基础:类试图和案例.ai-zh.srt 字幕整理结果
Django 类视图(Class-Based View)速通指南
1. 章节介绍
在 Django 中,如果只用函数视图(FBV),所有 HTTP 方法(GET、POST、PUT...)都会挤在一个函数里,代码臃肿且难以复用。
类视图(CBV)把"按请求方法分流"这件事自动化:一个类里写 get/post/put... 方法,路由只要绑定到 as_view(),Django 会在运行时自动派发到对应方法。
写法更 OOP,复用、扩展、单元测试都更方便,是 Django 工程里高频使用的核心技能。
| 核心知识点 | 面试频率 | 一句话速记 |
|---|---|---|
| CBV 与 FBV 区别 | 高 | 类视图=按方法分流+面向对象 |
View 基类与 as_view() |
高 | 入口函数,返回真正的视图函数 |
自定义 get/post/... |
高 | 方法名即 HTTP 方法名,大小写不敏感 |
路由绑定 path('url', MyView.as_view()) |
高 | 别忘了 as_view() 括号 |
JsonResponse 列表安全 |
中 | 列表需加 safe=False |
| 请求参数校验与异常返回 | 中 | 先校验→再业务→统一 JSON 格式 |
| Postman/DRF 测试 | 低 | 发 PUT、PATCH、DELETE 必备 |
2. 知识点详解
2.1 CBV 执行链(面试常考)
- 请求进入 WSGI → Django 路由匹配 →
MyView.as_view()返回_view闭包 _view实例化MyView()→ 调用dispatch()dispatch()根据request.method.lower()找到self.get/post...- 执行对应方法 → 返回
HttpResponse/JsonResponse
2.2 最小可运行模板
python
# views.py
from django.http import JsonResponse
from django.views import View
class DemoView(View):
def get(self, request):
return JsonResponse({'method': 'GET'})
def post(self, request):
title = request.POST.get('title')
if not title:
return JsonResponse({'status': 'error', 'msg': '标题不能为空'}, status=400)
return JsonResponse({'status': 'ok', 'id': 1})
# urls.py
from django.urls import path
from .views import DemoView
urlpatterns = [
path('demo/', DemoView.as_view()),
]
2.3 返回 JSON 列表陷阱
python
def get(self, request):
data = list(News.objects.values('id', 'title'))
return JsonResponse(data, safe=False) # 必须 safe=False
2.4 复用技巧:继承 + mixin
python
class JsonResponseMixin:
def success(self, data=None):
return JsonResponse({'status': 'ok', 'data': data or {}})
def error(self, msg, code=400):
return JsonResponse({'status': 'error', 'msg': msg}, status=code)
class CreateModelMixin:
def post(self, request):
# 通用新增逻辑
...
class NewsView(JsonResponseMixin, CreateModelMixin, View):
...
3. 章节总结
- 类视图 = 把"请求方法"映射成"类方法"
- 路由绑定一定要用
as_view() - 返回 JSON 列表记得
safe=False - 先写基类/mixin,再组合业务视图,代码量减半,测试通过率翻倍
4. 知识点补充
| 补充点 | 说明 |
|---|---|
dispatch() 可重写 |
做统一日志、权限、限流 |
csrf_exempt |
如果前端不携带 CSRF token,需在 as_view() 外包裹 |
method_decorator |
对 CBV 单个方法加装饰器 |
DRF APIView |
生产环境推荐,自带内容协商、解析器、异常处理 |
PUT/PATCH 参数 |
Django 原生不解析 request.PUT,需 QueryDict(request.body) 或 DRF |
4.1 最佳实践:可维护的 CBV 分层(≥300 字)
实际项目里,不要把所有逻辑都塞在 get/post 里。推荐三层:
- URL 层:只负责路由映射,不做任何业务判断。
- 视图层:只负责"请求分发 + 参数校验 + 响应格式化"。
- 服务层(service/service.py):真正的业务/DB 操作,方便单元测试。
示例目录:
news/
├── urls.py # path('news/', NewsView.as_view())
├── views.py # 只调用 service 并返回 JsonResponse
├── service.py # 增删改查、事务、外部 API 调用
├── serializers.py # 可选,用 DRF 时做字段校验
└── tests/
├── test_service.py # 测试 service 逻辑
└── test_views.py # 测试状态码、权限
这样做的好处:
- 视图类 30 行以内,易读易 Review。
- service 函数可随意加同步/异步、缓存、事务,而不影响视图。
- 写单测时,直接
from news.service import create_news,无需构造RequestFactory。 - 后续切到 DRF、GraphQL、甚至 gRPC,只需替换视图层,业务核心不动。
4.2 编程思想指导(≥300 字)
CBV 的本质是"把变化点封装起来"。HTTP 方法会变、权限会变、返回格式会变,但"找到变化并封装"的思想不变。
写 CBV 时,先问自己三个问题:
- 哪些东西未来会变?------返回格式、权限、数据源。
- 变得频率多高?------返回格式常变,权限偶尔变,数据源很少变。
- 变得方向是什么?------可能从 HTML 变 JSON,可能加缓存,可能切微服务。
回答完,就把"高频变化"抽象成 mixin 或策略类。例如:
- 把"返回 JSON"做成
JsonResponseMixin; - 把"权限"做成
OwnerRequiredMixin; - 把"分页"做成
PaginationMixin。
这样,当产品说"返回格式改成 protobuf"时,你只需要新增一个 ProtobufResponseMixin,原视图一行不改;当运营说"加缓存"时,只需在 dispatch() 里加一行 cache.get(key)。
记住:CBV 不是"写类"的炫技,而是"面向修改关闭,面向扩展开放"的 OCP 原则落地。把"稳定"留在 View,把"变化"隔离在 mixin,你就能在需求狂潮中优雅地"加代码而不改代码"。
5. 程序员面试题
| 难度 | 题目 | 参考答案 |
|---|---|---|
| 简单 | 函数视图与类视图在"请求方法分流"上的核心区别? | FBV 需手写 if request.method == 'GET',CBV 由 dispatch() 自动按方法名分流。 |
| 中等 | 为什么 JsonResponse([1,2,3]) 会抛异常?如何解决? |
默认 safe=True 只允许 dict,传 list 需显式 safe=False。 |
| 中等 | 如何在 CBV 里只对 post 方法关闭 CSRF 校验? |
@method_decorator(csrf_exempt, name='dispatch') 或自定义 dispatch 里对 self.post 加豁免。 |
| 困难 | 自定义 as_view() 时,怎样把额外参数(如 version=v1)注入到类实例? |
重写 as_view(),在 cls(**initkwargs) 时把自定义参数通过 **initkwargs 传进去,并在 __init__ 接收即可;注意线程安全,避免在实例变量里存请求级数据。 |
| 困难 | 给定一个高并发场景,CBV 里如何做到"根据请求 Header 中的 X-Use-Cache 决定是否走缓存",且不改业务方法? |
自定义 CacheMixin 重写 dispatch():先根据 request.headers.get('X-Use-Cache') 生成缓存 key,命中直接返回 HttpResponse(content, content_type='application/json'),未命中调用 super().dispatch() 并把结果写入缓存;视图类只需 class MyView(CacheMixin, View),业务代码零侵入。 |
把代码写成"以后自己三天就能看懂"的样子,就是 CBV 最大的价值。# [P20]框架基础:模版的使用.ai-zh.srt 字幕整理结果
Django 模板系统快速入门与实战
1. 章节介绍
本章从零搭建一个 Django 项目,重点讲解模板(Template)的完整工作流程:配置 → 渲染 → 展示。通过"新闻列表"实战,演示如何把后端模型数据注入到 HTML,帮助你在面试与真实项目中快速落地"前后端不分离"方案。
| 核心知识点 | 面试频率 | 一句话速记 |
|---|---|---|
| TEMPLATES 配置 | 高 | 告诉 Django 去哪里找 HTML |
| render() 三要素 | 高 | request、模板名、上下文 |
| 模板变量/标签/过滤器 | 高 | {``{ var }}、{% if %}、`{{ title |
| 模板继承 (extends) | 中 | 先整体后局部,减少重复 HTML |
| 静态文件引用 {% static %} | 中 | 解决 CSS/JS/图片路径问题 |
| csrf_token 防护 | 高 | 表单提交必加,防跨站请求伪造 |
| 自定义模板标签 | 低 | 业务逻辑太复杂时自己写标签 |
2. 知识点详解
2.1 TEMPLATES 配置
python
# settings.py
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # ← 关键:放 HTML 的目录
'APP_DIRS': True, # 允许到 app 下找模板
'OPTIONS': { ... }
}]
- DIRS 支持列表,可配多级目录;推荐项目级
templates/目录,团队协作最清晰。 - 修改后无需迁移,立即生效。
2.2 render() 函数
python
from django.shortcuts import render
def index(request):
news_list = NewsInfo.objects.filter(is_deleted=False)
return render(request, 'index.html', {'news': news_list})
三参数缺一不可:
① request -- 携带用户会话、csrf 等信息;
② 模板文件相对路径(相对于 DIRS);
③ 上下文字典 -- 模板里所有变量的"数据源"。
2.3 模板语言三大语法
- 变量
{``{ }}
调用对象属性或字典键,自动转义 HTML,防止 XSS。
关闭转义:|safe或{% autoescape off %}。 - 标签
{% %}
流程控制:for / if / elif / else / empty
模板继承:{% extends 'base.html' %}、{% block content %}{% endblock %} - 过滤器
{``{ value|filter:arg }}
常用:|date:'Y-m-d'、|truncatechars:20、|default:'暂无'
2.4 静态文件
html
{% load static %}
<link href="{% static 'css/news.css' %}" rel="stylesheet">
- 开发阶段
DEBUG=True时,Django 自动托管; - 生产环境
collectstatic统一收集到STATIC_ROOT,再由 Nginx 转发。
2.5 CSRF 中间件
默认开启,表单内必须加 {% csrf_token %},否则 POST 报 403。
面试常问:
"如果做成前后端分离,如何关闭或替代 CSRF?"
→ 可用 @csrf_exempt 或 CSRF_USE_SESSIONS=True 配合 JWT。
3. 章节总结
- 先配
DIRS,再写 HTML,最后render(request, 'xxx.html', context)。 - 模板语言 = 变量 + 标签 + 过滤器;牢记
{% csrf_token %}。 - 静态文件用
{% static %},上线务必collectstatic。 - 模型数据 → 视图查询 → 注入上下文 → 模板循环展示,是 Django 最经典 MTV 流程。
4. 知识点补充
4.1 高频踩坑点
- 模板找不到 → 检查
DIRS/ 拼写 / 是否忘记app_dirs。 - 静态 404 → 确认
DEBUG=True或 Nginx 路径映射。 - 中文乱码 → HTML 加
<meta charset="utf-8">,文件本身保存为 UTF-8。 - for 循环空列表 → 用
{% empty %}分支避免页面空白。 - 修改模板不生效 → 重启或检查缓存中间件。
4.2 最佳实践(≥300 字)
模板目录规划
大型项目务必"项目级 + 应用级"两级目录:
templates/
base/ # 全站母版
includes/ # 可复用小组件(pagination、footer)
news/
templates/
news/
list.html
detail.html
命名规范
- 母版:
base.html - 功能模块:
appName_modelName_action.html,如news_article_detail.html
继承层级
base.html只放<html><head><body>框架与全局 CSS/JS;- 每个业务模块再建
module_base.html,继承全局 base,再定义{% block module_style %}; - 最终页面只关注自身内容,不重写公共代码。
数据注入原则
视图只查"必要字段",禁止select *;模板里禁止写.all()二次查询(N+1),提前select_related/prefetch_related。
性能优化
- 开启
django.template.loaders.cached.Loader缓存模板解析结果; - 用
{% verbatim %}包裹前端框架(Vue/React)插值,避免冲突; - 大图走 CDN,CSS/JS 文件名加 hash,利用浏览器长缓存。
4.3 编程思想指导(≥300 字)
"表现与逻辑分离"
模板 = 表现层,只负责"如何展示";视图 = 控制层,决定"展示什么"。严禁在模板内写 SQL、复杂计算。
"上下文最小化"
传递给模板的数据结构要扁平、精确。与其传整个 User 对象,不如只传 {'username': user.username},降低模板出错概率,也利于单元测试。
"继承优于复制"
80% 的页面结构相同,先抽象出 base.html,再逐步细化。发现三处以上重复,立即提取为 include 或自定义标签。
"防御式编程"
所有外部数据(用户输入、模型字段)在模板里默认"不可信"。
- 用
{``{ value|default:'' }}兜底; - 用
{% if %}判断存在性,避免DoesNotExist; - 用
{% url %}反向解析,杜绝硬编码路径。
"可替换性"
即使未来要迁移到前后端分离,只要视图返回统一 JSON,模板层可随时被 Vue/React 替换,而模型与业务逻辑无需改动。保持接口契约稳定,是架构演进的底气。
5. 程序员面试题
| 难度 | 题目 | 参考答案 |
|---|---|---|
| 简单 | 写出 Django 渲染模板 index.html 的函数调用 |
render(request, 'index.html', {'data': data}) |
| 中等 | 模板中如何防止 XSS?关闭转义有哪些方法? | 默认 {``{ content }} 已转义;用 ` |
| 中等 | 解释 {% csrf_token %} 的作用及实现原理 |
在表单中生成隐藏字段 + cookie,提交时中间件比对两者值,防 CSRF 攻击。 |
| 困难 | 生产环境静态文件 404,如何排查与解决? | ① 确认 DEBUG=False;② 运行 python manage.py collectstatic;③ 检查 STATIC_ROOT 与 Nginx location /static/ 路径映射;④ 确认文件权限与 STATIC_URL 设置一致。 |
| 困难 | 模板出现 N+1 查询,如何定位并优化? | 用 Django Debug Toolbar 查看 SQL 次数;在视图中使用 select_related(一对一/外键)或 prefetch_related(多对多/反向查询)一次性取回关联数据;模板内避免对查询集循环再 .all()。