【Django】基础1(万字讲解)

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 备用
  • 重点先行:
    1. Tutorial(polls 项目)
    2. Model layer
    3. View layer
    4. Template layer
    5. 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)
  • 迁移命令:makemigrationsmigrate
  • 默认支持 SQLite,零配置即可启动;生产切 PostgreSQL/MySQL 仅需改 DATABASES 字典

3. 章节总结

  1. pip install Django==3.2.18 锁定长期支持版
  2. 官方文档切"3.2+中文"作为第一手资料
  3. Django 按 MVT 分层:Model 管数据、View 管业务、Template/JSON 管表现
  4. 路由 → 视图 → (模型+模板/序列化器) → 响应 是面试必画生命周期
  5. 根据团队技能与 SEO 需求,在"模板渲染"与"DRF 分离"间灵活切换

4. 知识点补充

4.1 补充 5 个高频延伸

  1. WSGI vs ASGI:Django 3.2 默认 WSGI,4.x 推荐 ASGI 以支持 WebSocket
  2. settings 拆分:base.py / dev.py / prod.py 12-Factor 配置管理
  3. App 划分原则:按业务分 app,避免"万能 app",保持解耦
  4. Django 中间件:5 个钩子方法,典型用途日志、权限、CORS
  5. Django 自带后台:一句 admin.site.register(Model) 生成运营级后台,演示原型利器

4.2 最佳实践(≥300 字)

生产环境部署 Django 时,务必"先静态后动态、先缓存后数据库"。

  1. 静态文件:使用 python manage.py collectstatic 统一收集到 STATIC_ROOT,再由 Nginx 直接托管,避免 Django 处理 GET /static/ 压力
  2. 反向代理:Nginx → gunicorn/uwsgi 的 UNIX socket,比 127.0.0.1:8000 少一次 TCP 握手;gunicorn workers 数设 CPU×2+1,并开启 --preload 预加载代码
  3. 数据库连接池:Django 3.2 原生不支持连接池,使用 django-db-geventpoolpgbouncer 防止高并发下"too many connections"
  4. 缓存层:对读多写少接口加 cache_page(timeout=60*15),或手动 cache.set(key, data, 900);更新数据时先删缓存再写库,保证最终一致
  5. 日志:统一 dictConfig,区分 django.request / django.db.backends,日志落盘 + ELK 收集,方便链路追踪
  6. 安全:关闭 DEBUG=TrueALLOWED_HOSTS 精确到域名;使用 django-environ 把 SECRET_KEY 放到环境变量;HTTPS 强制 HSTS
  7. 监控:Prometheus + django-prometheus 暴露 /metrics,配合 Grafana 面板实时观察 5xx、响应时间、SQL 平均耗时
  8. 灰度:利用 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() 延迟加载,必要时用 RawSQLextra()

这告诉我们:框架是 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 项目 & 应用创建

  1. 创建项目:django-admin startproject project_name
  2. 进入项目根目录,创建应用:python manage.py startapp app_name
  3. 将应用注册到 INSTALLED_APPS,否则模型无法被识别。

2.2 切换 MySQL 数据库

  1. 手动建库:CREATE DATABASE dbname CHARACTER SET utf8mb4;

  2. 安装驱动:

    • 官方推荐:pip install mysqlclient(需系统依赖)

    • 纯 Python 备选:pip install PyMySQL,并在 __init__.py 中:

      python 复制代码
      import pymysql
      pymysql.install_as_MySQLdb()
  3. 修改 DATABASES:

    python 复制代码
    DATABASES = {
        '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 → 数据库允许 NULL
  • blank=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. 章节总结

  1. 先建项目→应用→注册应用;
  2. 手动建 MySQL 库,改 ENGINE 与连接参数,装驱动;
  3. models.py 中写类 → 选字段 → 加选项;
  4. makemigrations + migrate 一键成表;
  5. 后续只需改模型再迁移,即可持续演进表结构。

4. 知识点补充 & 最佳实践

4.1 补充知识

  1. Meta 内部类:指定 db_tableindexesorderingverbose_name_plural 等。
  2. 复合索引:models.Index(fields=['status', '-created']) 提升多列查询速度。
  3. 软删除:新增 is_deleted 字段,重写 delete() 方法做逻辑删除,避免误删。
  4. 分表策略:单表千万级后,可按时间/哈希拆分,结合 Django 多数据库路由。
  5. 自定义字段:继承 Field 实现 JSONField(Django<3.1 时)、EncryptedCharField 等。

4.2 最佳实践(≥300 字)

"模型是项目的地基"------设计阶段多花 10 分钟,能避免上线后 10 倍的返工。

  1. 命名规范:表名用复数小写 news_article,字段用蛇形 pub_date;类名用驼峰 NewsArticle,保持一致性。
  2. 主键策略:默认自增 id 即可;分布式场景再改用 BigAutoField 或雪花 ID,避免后期迁移痛苦。
  3. 货币与精度:金额一律 DecimalField,禁止 FloatField 防止精度丢失;同时配合 positive 校验。
  4. 时间字段:创建时间 auto_now_add=True,更新时间 auto_now=True,确保审计轨迹。
  5. 索引意识:对外键、查询条件、排序字段建索引;但控制总数,写多读少场景需权衡。
  6. 迁移纪律:每功能一小步,禁止手动改表结构;迁移文件入库,方便多环境回放。
  7. 数据种子:用 fixturesfactory_boy 生成测试数据,保持 CI 可重复。
  8. 安全默认:null=False, blank=False 先收紧,必要时再放开;敏感字段加密后再入库。
  9. 版本兼容:字段新增先允许空,代码双版本兼容,灰度完成后再补非空约束。
  10. 文档同步:在 help_textverbose_name 写清业务含义,自动生成后台文档,降低沟通成本。

4.3 编程思想指导(≥300 字)

ORM 的核心思想是"把关系数据库映射成对象",让开发者用面向对象思维写 SQL,从而提高抽象层级与可维护性。

  1. 抽象优先:先思考"对象是什么、职责是什么",再决定表结构;而非先画表再硬套对象。
  2. 单一职责:一个模型只负责一块业务域;不要把用户、订单、日志揉在一个类里。
  3. 充血模型:把与自身数据强相关的行为(如 get_absolute_url()increase_views())放到模型类/管理器里,避免 Service 层臃肿。
  4. 延迟加载:利用 QuerySet 懒查询特性,先过滤再取值,减少 N+1;必要时用 select_related/prefetch_related 批量Join。
  5. 领域即语言:好的模型就是业务语言,Article.objects.published().hot() 让代码自解释。
  6. 迁移即版本:把每一次 schema 变更当成一次 commit,可回滚、可 review、可追踪。
  7. 性能意识:ORM 不是银弹,复杂报表仍可用原生 SQL 或视图,但要把结果再封装成模型,保持接口统一。
  8. 测试护航:为模型写单元测试,验证约束、默认值、信号触发;用 TransactionTestCase 隔离测试数据。
  9. 演进式:业务变化时,宁可加新字段也不要改旧字段含义;废弃字段先标记 deprecated,数个版本后再清理。
  10. 多数据库:读写分离、分片、异地多活时,用 Django 数据库路由把细节藏在框架层,上层业务代码零感知。

5. 程序员面试题

  • 简单

    创建 Django 模型 Book,包含书名、作者、价格、出版日期,请写出模型代码。

    python 复制代码
    from 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=Trueblank=True 的区别,并举例何时同时用。
    null=True 表示数据库字段允许存储 NULL 值;blank=True 表示 Django 表单验证时允许该字段为空(即非必填)。二者作用层面不同,前者针对数据库,后者针对表单逻辑。例如,一个可选的过期时间字段可以同时设置两者:

    python 复制代码
    expire_time = models.DateTimeField(null=True, blank=True)
  • 中等

    如何在已有百万级数据的 User 表上安全新增非空 nickname 字段?

    1. 首次迁移:新增字段并允许为空:

      python 复制代码
      nickname = models.CharField(max_length=30, null=True, blank=True)
    2. 编写数据填充脚本,为已有记录设置默认值(如"用户123")或从其他字段生成;

    3. 第二次迁移:修改字段为非空(null=False);

    4. 为确保安全,上线前应进行灰度发布,并在代码中兼容新旧字段状态。

  • 困难

    说明 Django 迁移系统工作原理,并手写一个自定义迁移文件,给 Article 表的 status 字段创建复合索引 (status, -created)
    原理 :Django 根据模型定义生成迁移文件(.py),记录数据库结构变更操作。执行 migrate 时,Django 读取未应用的迁移文件,执行对应 SQL,并将已执行记录写入 django_migrations 表以避免重复执行。
    自定义迁移步骤

    bash 复制代码
    python manage.py makemigrations --empty your_app_name

    然后编辑生成的空迁移文件:

    python 复制代码
    from 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') 导致全表扫描,如何优化?

    1. 确保 user 字段已建索引(ForeignKey 默认有索引,但可显式确认);

    2. 添加复合索引覆盖查询条件和排序字段:

      python 复制代码
      class Meta:
          indexes = [
              models.Index(fields=['user', '-created'], name='order_user_created_idx')
          ]
    3. 执行迁移后,使用 EXPLAIN 验证 SQL 是否命中索引;

    4. 若数据量极大,可考虑分页优化(如基于游标的分页)、冷热数据分离,或使用覆盖索引减少回表查询。


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 迁移与中间表

  1. python manage.py makemigrations # 生成迁移文件
  2. python manage.py migrate # 真正建表
  3. M-N 关系会自动生成 app_model1_model2 中间表,包含两列 *_id

3. 章节总结

  1. 先画 ER 图 → 再写 Model → 再迁移;字段选型、help_text、索引一步到位
  2. 1-N 用 ForeignKey 放在"多"端;M-N 用 ManyToManyField 任意端;1-1 用 OneToOneField
  3. 迁移后务必检查数据库,确认中间表、外键索引、字段属性符合预期

4. 知识点补充

4.1 额外 5 个高频考点

  1. related_name & related_query_name:反向查询别名,避免冲突

  2. on_delete 选项:CASCADE / PROTECT / SET_NULL / SET_DEFAULT 面试必问

  3. through 自定义中间表:需要给关系加额外字段(如"打标签时间")

  4. self 自关联:评论回复、无限级分类

    python 复制代码
    class Comment(models.Model):
        ...
        parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
  5. Meta 内部类:db_table、ordering、indexes、unique_together、verbose_name

4.2 最佳实践(≥300 字)

场景:日均千万级阅读的新闻站点,要求

  • 后台可实时查看"某类型下阅读量 Top100"
  • 外键不能拖慢查询
  • 模型改动可灰度迁移

实践步骤

  1. 字段设计
    • 读/写分离:read_cnt 用 PositiveIntegerField,避免负值;同时建 read_cnt_idx 联合索引 (category_id, -read_cnt)
    • 标题长度按业务 90% 分位值给 120,不盲目 255;content 用 TextField + 额外 ES 索引,不建 DB 索引
  2. 关系设计
    • 1-N 外键显式声明 db_index=True(Django 4.1+ 已默认,但显式写出可读性高)
    • M-N 用默认中间表即可;若需"加标签时间"则自定义 through 表,并加 created_at 字段
  3. 迁移策略
    • 大表加字段使用 AddField(migrations.AddField(...), preserve_default=False) + 灰度双写
    • 新建索引使用 migrations.AddIndex(..., condition='CONCURRENTLY')(PostgreSQL)避免锁表
  4. ORM 查询
    • 列表页用 select_related('category') 一次性拿外键,N+1 → 1 查询
    • Top100 直接走覆盖索引:News.objects.filter(category=xxx).order_by('-read_cnt')[:100]
  5. 代码规范
    • 统一 verbose_name / help_text,后台自动生成友好表单
    • 模型文件名 models/news.py,避免单文件过大

遵循以上规范,可在高并发快速迭代之间取得平衡,同时让面试答案"有数据、有案例、有量化"。

4.3 编程思想指导(≥300 字)

  1. "先关系,后字段"

    很多开发者一上来就写字段,结果外键放错端。正确顺序:先识别实体 → 确定关系 → 再补字段。ER 图哪怕手绘,也能让 ForeignKey 位置一目了然。

  2. "把规则显式化"
    on_delete=models.PROTECT 比 CASCADE 安全,但很多人图方便直接 CASCADE。把业务规则显式写到代码里,既自文档又防误操作。

  3. "迁移可逆"

    每次 makemigrations 后,本地先 migrate → 再 migrate zero,确认可回滚。生产环境灰度发布才能做到"出问题 30 秒内回滚"。

  4. "性能提前设计"

    面试常问"如果数据量扩大 100 倍怎么办?"------在模型层就给出答案:

    • 索引、联合索引、覆盖索引
    • 自增 ID 与雪花 ID 的权衡
    • 分库分表中间件与 ShardingKey 选取
  5. "ORM 是 DSL,不是黑盒"

    queryset.query 打印 SQL,用 explain 分析索引;把 ORM 当"生成 SQL 的 DSL",而不是"屏蔽 SQL 的魔法"。理解其生成规则,才能写出既优雅又高效的代码。


5. 程序员面试题

  • 简单

    写一段 News 模型,包含标题、正文、发布时间,并给标题加索引。

    python 复制代码
    from 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 模型中包含指向 CategoryForeignKey。这样在数据库层面只需在 news 表加一个外键列,无需额外中间表,且 Django ORM 支持通过反向关系(如 category.news_set.all())便捷地查询所有相关新闻。

  • 中等
    ManyToManyField 实际会生成几张表?如何自定义中间表?

    默认会生成 1 张中间表,仅包含两个外键字段。若需在中间表中添加额外字段(如创建时间、备注等),应使用 through 参数自定义中间模型:

    python 复制代码
    class 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 亿,要按"分类+发布时间"分页查询,如何设计索引与模型?

    1. 在数据库层面创建联合索引:(category_id, -pub_time),确保查询和排序高效;

    2. 避免深度分页(如 OFFSET 1000000),改用基于游标的分页(如 WHERE category_id = X AND pub_time < last_seen_time);

    3. 使用覆盖索引(INCLUDE 或选择必要字段)减少回表;

    4. 考虑按 category_id 对表进行水平分区(或分库分表),分散 I/O 压力;

    5. 在 ORM 查询中限制字段,例如:

      python 复制代码
      News.objects.filter(category=cat).order_by('-pub_time').only('id', 'title')[:20]
  • 困难

    解释 on_delete=models.SET(func) 的用途,并给出伪代码。
    on_delete=models.SET(func) 表示当被引用的对象被删除时,Django 会调用 func 函数,并将外键字段设为该函数的返回值。常用于"软删除"后自动迁移关联数据。
    伪代码示例

    python 复制代码
    def 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 后台汉化 & 时区

settings.py

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. 章节总结

  1. Meta.db_table 让表名符合团队规约。
  2. verbose_name* 让 Admin 显示中文,告别"Object"。
  3. 给模型写 __str__,让调试、下拉框可读。
  4. 自定义 ModelAdmin,把常用字段、过滤、搜索一次性配好。
  5. 创建超级用户并汉化,为运营/产品提供友好录入环境。

4. 知识点补充

4.1 额外必知 Meta 选项

  1. ordering = ['-id']:默认倒序,减少视图层 .order_by() 重复代码。
  2. indexes = [models.Index(fields=['created_at'])]:复合索引,加速时间范围查询。
  3. abstract = True:公共父模型,不产生表,常用于时间戳、软删除基类。
  4. app_label = 'cms':模型文件不在标准 apps.py 时显式归属。
  5. constraints = [models.UniqueConstraint(...)]:联合唯一约束,替代 unique_together。

4.2 最佳实践:可维护的"新闻信息"模型设计(≥300 字)

在真实项目中,模型设计第一目标是"可读 + 可演 + 可迁移"。以新闻系统为例,建议采用如下结构:

  1. 表名统一前缀:公司规范 cms_newscms_news_type,避免与支付、订单表混杂。
  2. 主键用默认自增 id,但暴露给前端的用 hashids 编码,防止爬虫顺序遍历。
  3. 状态字段用 SmallIntegerField + choices,如 STATUS_CHOICES = ((1, '草稿'), (2, '已发布')...;Admin 内用 list_filter 快速筛选。
  4. 时间字段统一用 DateTimeField(auto_now_add=True)auto_now=True,并在 Meta.ordering 里写 ['-publish_at'],保证默认列表最新在前。
  5. 正文若超 5k 字,拆独立 NewsContent 模型,一对一,避免大文本拖慢列表查询。
  6. 多对多通过显式中间表 NewsTypeRelation 加权重字段,方便后续"主类型""次类型"排序。
  7. 阅读量、点赞量等高并发字段,单独拆 Redis 计数,通过定时任务回写 DB,模型只保留快照字段。
  8. 国际化:标题、正文存 JSONField(PostgreSQL)或单独 NewsI18n 表,按语言 code 查询。
  9. 软删除:加 is_deleted + deleted_at,重写自定义 Manager 过滤掉已删数据,Admin 另配 DeletedFilter 供运营恢复。
  10. 版本化:每发布一次生成 NewsVersion,支持一键回滚;Admin 内用 TabularInline 展示历史版本。

这样,模型上线后可平滑支持:灰度发布、多语言、高并发读、数据恢复,且迁移文件清晰,字段语义明确,新人 5 分钟即可读懂。

4.3 编程思想指导:模型即接口(≥300 字)

Django 模型不只是"数据库表翻译器",更是 面向业务对象的接口。写好模型,等于给整个项目定下"领域方言"。思想要点:

  1. 领域优先:先和业务方对齐"新闻""类型""标签""栏目"概念,再落地字段;切忌一上来就考虑索引、缓存。
  2. 高内聚低耦合:模型只负责"自己"数据与基础行为(如 __str__get_absolute_url)。发邮件、调用第三方 API 放到 service 层或 Celery 任务。
  3. 显式优于隐式:verbose_name、help_text、choices 都写全,Admin 里就能自解释;半年后你自己也忘了 0/1/2 代表啥。
  4. 默认安全:BooleanField 默认值务必给出;CharField 加 max_length 限制;DecimalField 用货币场景,避免 Float 精度坑。
  5. 面向查询设计:经常 list_filter 的字段就加 db_index;模糊搜索用 PostgreSQL 的 GinIndex + icontains,MySQL 则考虑全文索引或 ES。
  6. 演进式:小步快跑,每加一个字段就写迁移、写 Admin、写单元测试,避免"一口气加 20 个字段"导致回滚困难。
  7. 元编程慎用:重写 __getattribute__、动态给类挂字段,会让调试难度指数级上升;除非写框架,否则别炫技。
  8. 文档即代码:在模型 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。

  • 常见写法:

    python 复制代码
    qs = 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 三大特征(面试常深挖)

  1. 惰性(Lazy)

    仅当"真正需要数据"时才发 SQL,例如:

    python 复制代码
    qs = User.objects.filter(level=3)   # 0 次 SQL
    print(qs)                           # 1 次 SQL
  2. 缓存(Cache)

    同一个 QuerySet 第二次迭代会复用第一次的缓存,不会重复查库。

  3. 链式(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 个高频考点

  1. values() / values_list():取指定字段,返回 dict / tuple 序列,减少序列化开销。
  2. select_related():一对一、多对一预加载,把 JOIN 提前做掉,解决 N+1。
  3. prefetch_related():多对多、反向一对多预加载,用 IN 查询拆成两条 SQL,同样解决 N+1。
  4. exists():比 len(qs)bool(qs) 更快,只做 SELECT (1) AS "a" FROM ... LIMIT 1
  5. 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。

  1. 先画 ER 图,理清实体关系;再写 Django Model,把字段类型、db_index、unique_together 一次定义到位------这叫"模型即文档",让后续开发者一眼看懂业务。
  2. 查询时坚持"语义化":filter 里只写"业务含义",不写魔法数字。例如 status=2 让人看不懂,封装成 Article.ONLINE 常量或 QuerySet.online() 方法,既可读又防错。
  3. 利用"链式接口"做"渐进式"构造:把公共过滤逻辑写成可复用的 QuerySet 方法(自定义 Manager),如 Article.objects.by_user(user).online().in_days(7),让视图层只关心组合,不关心细节。
  4. 牢记"懒加载"双刃剑:一方面避免过早发 SQL,另一方面要警惕"循环里不小心触发查询"。标准做法是:在视图层把需要的数据一次性取完,再传给模板或序列化器;禁止在模板/序列化器里再点外键。
  5. 最后,任何"性能优化"都要先度量后动手:用 count(*)、explain、慢日志确认瓶颈,再决定是加索引、改查询,还是拆表、拆库。ORM 让你更快迭代,但别让"看不见"的 SQL 拖垮整个系统------先思考关系,再写代码;先度量,再优化,这是资深工程师与新手最大区别。

5. 程序员面试题

  • 简单
    get()filter() 的核心区别?
    get() 用于获取唯一匹配 的模型实例:如果查询结果为空或多于一个,会分别抛出 DoesNotExistMultipleObjectsReturned 异常。
    filter() 返回一个 QuerySet,可包含零个、一个或多个对象,不会抛异常,适用于列表查询。

  • 中等

    如何把一个 QuerySet 按字段 created_time 降序、再按 id 升序排列?

    python 复制代码
    qs = 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() 等求值方法
  • 困难

    线上接口出现 N+1,如何定位并修复?请给出至少两种不同关系的解决代码。
    定位方法

    • 使用 django-debug-toolbar 查看 SQL 执行次数;
    • 日志中观察重复的单条查询(如循环中频繁查外键)。

    修复方案

    1. 多对一(正向外键) :使用 select_related(单次 JOIN)

      python 复制代码
      articles = Article.objects.select_related('author').all()
      # 避免在循环中访问 article.author 触发额外查询
    2. 一对多(反向关系)或 多对多 :使用 prefetch_related(额外查询后内存关联)

      python 复制代码
      categories = Category.objects.prefetch_related('article_set').all()
      # 或
      users = User.objects.prefetch_related('groups').all()
  • 困难

    请写出一段代码,仅查询 User 表中的 id, username 两列,并返回列表形式的字典,要求 SQL 只查一次且不带缓存。

    python 复制代码
    result = 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 范围查询

  1. in:多值枚举
    id__in=[1, 3, 5]WHERE id IN (1,3,5)
  2. 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=TrueIS NULL
  • field__isnull=FalseIS NOT NULL

注意:Django 里空字符串 '' 与 NULL 是两种状态;仅当 null=True 才可能出现 NULL。


3. 章节总结

一张图背下所有写法:

复制代码
filter(字段__关键字=值)
关键字:contains、startswith、endswith、
        in、range、gt、gte、lt、lte、isnull

4. 知识点补充

4.1 必须补充的 5 个细节

  1. icontains / istartswith / iendswith:不区分大小写版本。
  2. regex / iregex:直接写正则,适合复杂模式。
  3. date / year / month / day:按日期部分过滤,如 pub_time__year=2023
  4. exclude():取反过滤,等价于 NOT
  5. 索引失效:前导模糊 (LIKE '%xx') 会使 MySQL 索引失效,大数据量请用全文检索 (Elasticsearch、Whoosh)。

4.2 最佳实践(≥300 字)

场景 :千万级文章库,按标题模糊搜"科技"并只返回最近一年数据。
问题title__contains='科技' 在数据量大时全表扫描,接口 3 s+。
优化步骤

  1. 模型层添加 db_index=Truepub_time 建普通索引;title 不建,因为 LIKE '%科技%' 用不到 B+Tree。

  2. 先用时间索引缩小范围,再模糊匹配:

    python 复制代码
    recent = timezone.now() - timedelta(days=365)
    qs = (Article.objects
          .filter(pub_time__gte=recent)      # 走索引,先剪 95 % 数据
          .filter(title__icontains='科技'))  # 仅扫描剩余 5 %
  3. 列表接口必加分页:qs[:20] 或使用 Paginator;避免一次性 len(qs)

  4. 模糊搜索升级:接入 Elasticsearch,Django 端用 django-elasticsearch-dsl,把 title 设为 text + ik_max_word 分词,查询改走 search=Search().query('match', title='科技'),RTT < 100 ms。

  5. 缓存:热搜关键词每日预热,直接缓存 QuerySetvalues_list('id', flat=True) 20 k 条,命中缓存则回源 ID 二次查询,减少 80 % 数据库压力。

结论:条件查询语法简单,但大数据场景必须"先过滤高选择性字段,再模糊匹配",并配合索引、分页、缓存或搜索引擎,才能兼顾功能与性能。

4.3 编程思想指导(≥300 字)

  1. 把 ORM 当"编译器"

    任何 filter() 都只是生成 SQL 的 AST,不要急于"优化得过早"。先用 print(qs.query) 观察真实 SQL,再用 explain 分析执行计划,最后决定加索引或改写法。

  2. "关键字"思维模型

    所有双下划线关键字本质是对 SQL 表达式的"命名封装"。记住映射关系即可举一反三:gt>rangeBETWEENisnullIS NULL。面试手写代码时,先在草稿纸写出 SQL,再反向推 ORM,准确率提升 50 %。

  3. 组合优于继承

    复杂查询用 Q() 对象组合,而不是写长串 if/else 拼接。Q(title__contains='科技') | Q(tag__name='AI') 可读性远高于字符串拼接,也能避免 SQL 注入。

  4. "延迟求值"+"切片即触发"
    QuerySet 只在真正需要数据时才发 SQL(迭代、切片、list()len())。利用这一特性,在视图层先统一加 select_related/prefetch_related 做 N+1 防护,再返回给前端,减少 90 % 冗余查询。

  5. 测试即文档

    为每个常用条件写单元测试,既防回归,又能在面试时直接引用 GitHub 链接作为"可运行简历"。示例:

    python 复制代码
    @pytest.mark.django_db
    def test_news_contains():
        NewsType.objects.create(name='科技新闻')
        assert NewsType.objects.filter(name__contains='科技').exists()

    运行通过即证明语法正确,比口头背诵更可信。


5. 程序员面试题

  • 简单

    写出 ORM 查询"标题包含'Django'"的语句。

    python 复制代码
    Model.objects.filter(title__contains='Django')
  • 中等

    查询 id[10, 20, 30] 且发布时间在过去 30 天内的文章。

    python 复制代码
    from 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"?分别写出两种查询。

    • 查询空字符串:

      python 复制代码
      Model.objects.filter(title='')
    • 查询 NULL 值:

      python 复制代码
      Model.objects.filter(title__isnull=True)
  • 困难

    title__contains='关键词' 造成全表扫描时,你会如何优化?至少给出 3 种方案。

    1. 缩小查询范围 :先通过时间、状态等高频过滤字段加索引筛选子集,再对子集做 contains
    2. 引入全文搜索引擎:如 Elasticsearch 或 Meilisearch,替代数据库模糊查询;
    3. 使用数据库原生全文索引 (如 MySQL 8.0 的 InnoDB 全文索引),配合 title__search='关键词'
    4. 缓存热门搜索词结果,降低数据库压力;
    5. 限制返回字段 ,使用 .values('id', 'title') 减少网络与内存开销(虽不解决全表扫描,但提升整体性能)。
  • 困难

    Q 对象实现:标题含"Python"或标签为"后端",且 id 大于 100。

    python 复制代码
    from 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)

  1. 作用:把"字段"当变量直接放进 SQL 表达式,实现行内字段比较原子更新
  2. 必须 from django.db.models import F 后使用。
  3. 典型场景
    • 字段自增、自减(阅读量+1,库存−1)。
    • 字段间比较(找出阅读量 > 评论量 2 倍的文章)。
    • 跨表比较(F('author__level'))。
  4. 算术 / 位运算支持
    + - * / % **| & ^、以及 Func() 表达式均可链式追加。
  5. 并发安全
    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)

  1. 作用:构造逻辑与或非 查询,突破 filter(a=1, b=2) 只能 AND 的限制。
  2. 运算符
    • & 逻辑与
    • | 逻辑或
    • ~ 逻辑非
  3. 基本范式
    Model.objects.filter(Q(...) | Q(...))
  4. 动态拼接
    利用 reduce(operator.or_, q_list) 快速把列表条件拼成"大 OR"。
  5. 与 filter/exclude 混用规则
    • 一旦显式出现 Q,关键字参数 必须与 Q 用 & 连接,否则行为未定义。
  6. 空 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. 知识点补充

  1. Func() 表达式:在 F 基础上调用数据库函数,如 Upper(F('name'))
  2. Case/When 条件表达式:配合 F/Q 实现"if-else" SQL。
  3. Window 窗口函数:F 对象可作为 partition_by / order_by 字段。
  4. select_for_update(skip_locked=True):高并发库存扣减标配。
  5. 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

要点

  1. 单条 UPDATE ... WHERE stock>0 利用行级锁原子性解决竞态。
  2. rows 返回受影响行数,0 即库存已空,业务立即回滚。
  3. 无需 select_for_update,减少一次 SELECT,提高 TPS。
  4. 若还需记录订单,把 insert into order 放在同一事务,保证库存-订单一致。
  5. 对热点商品,可再叠加 Redis 预减库存 + MQ 异步落库 ,但数据库层仍用 F 对象做最终兜底,确保数据强一致

编程思想指导(≥300 字)

  1. "让数据库做它擅长的事"

    字段比较、算术、布尔组合都是 SQL 的天然能力。把运算推向 DB,减少网络 IO 与 Python 内存,是性能优化的第一性原则

  2. "声明式优于命令式"

    F/Q 对象让你**声明"要什么"**而非"怎么遍历"。代码从 10 行 for-loop 变成 1 行表达式,既简洁又利用底层索引。

  3. "并发控制要靠近数据"

    传统"读-改-写"在应用层加锁,距离数据远,锁粒度粗。F 对象的单语句更新把并发控制下沉到 InnoDB 行锁,粒度更细,冲突更少。

  4. "动态条件用数据结构描述"

    面对前端 20 个筛选项,不要写 20 个 if-else 拼 filter。统一转成 Dict[str, Any],再转 Q(**dict)reduce(or_, q_list),既易维护又避免 SQL 注入。

  5. "认知复杂度 ≈ 括号深度"

    当 Q 对象嵌套超过 3 层,或一个 filter 里出现 5 个以上 Q,立刻拆函数 。把"业务语义"封装成 build_user_q(params)build_date_q(start, end),再组合,既单测友好又降低心智负担。


5. 程序员面试题

  • 简单

    如何用 F 对象把文章阅读数原子地 +1?

    python 复制代码
    from django.db.models import F
    Article.objects.update(read_num=F('read_num') + 1)

    该操作在数据库层面执行原子更新,避免竞态条件。

  • 中等

    写出"找出库存大于销量 2 倍且状态为在售"的 ORM 语句。

    python 复制代码
    from django.db.models import F
    Goods.objects.filter(stock__gt=F('sold') * 2, status='on_sale')
  • 中等

    用户传入标签列表 ['py', 'dj'],要求文章满足任一标签即返回,如何动态构造 ORM?

    python 复制代码
    from 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,即超卖。

    解决方案(原子扣减 + 乐观锁):

    python 复制代码
    updated = 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 写法

    python 复制代码
    from 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_numcomment_num 上建单列索引;
    • 更优方案是建立两个联合索引
      • (id, read_num)
      • (id, comment_num)
        利用 id 作为前缀加速 NOTOR 条件的过滤;
    • 若数据量极大,可考虑将条件拆分为两个 union() 查询,分别走不同索引。

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 做整体统计,返回一个字典
  • annotateGROUP BY 再对每组统计,返回每行附加聚合字段的 QuerySet

3. 章节总结

  1. from django.db.models import Max, Min, Avg, Sum, Count
  2. 在任意 QuerySet 后链 .aggregate(...),可一次传多个聚合类
  3. 结果是一个字典,key 可自定义;无数据时不会抛异常,值可能为 None
  4. 统计行数优先用 Count('id')Count('*') 同理
  5. 需要分组统计换用 values(...).annotate(...)

4. 知识点补充

4.1 补充知识

  1. 条件聚合:aggregate(total=Sum('amount', filter=Q(status=1)))
  2. 去重计数:Count('author', distinct=True)
  3. 原生 SQL 对比:SELECT MAX(comment_count) FROM news;
  4. 事务一致性:聚合与快照读在同一事务,可重复读隔离级别保证数据一致
  5. 性能优化:对聚合字段加索引可加速 MAX/MINCOUNT(*) 在 MySQL InnoDB 仍需行扫描,可借助冗余统计表或缓存

4.2 最佳实践(≥300 字)

线上报表接口经常需要「昨日新增 / 峰值 / 均值」等多指标一次性返回。若循环查询 N 次,既增加 RT 又占用连接池资源。最佳实践是:

  1. 只查一次数据库,用 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')
)
  1. 对过滤字段 create_time 建立联合索引,确保 WHERE + AGG 走覆盖索引
  2. 结果字典直接塞进 Redis 缓存,设置 TTL=1h,防止并发接口反复打数据库
  3. 若数据量达百万级,考虑物化视图或定时落库「日统计表」;接口直接查统计表,毫秒级返回
  4. 代码层做好 None 兼容,如 stats['avg_read'] or 0,前端展示更友好

通过「一次聚合 + 缓存 + 索引」三板斧,把统计接口 RT 从 800ms 降到 30ms,数据库 CPU 下降 45%,可支撑 10 倍流量增长。

4.3 编程思想指导(≥300 字)

聚合函数的核心思想是让计算靠近数据。数据库用 C/C++ 实现,跑在原生机器码层面,把 10 万条整型求和从 Python 循环的 O(n) 级解释器开销,转化为本地 SIMD 指令,性能提升可达两个数量级。作为工程师应牢记:

  1. 能一条 SQL 解决的,绝不拉回 Python for-loop;网络 IO + 解释器循环是性能杀手
  2. 聚合本质是 MapReduce 的局部 Reduce,先下推至存储引擎,大幅削减传输数据量
  3. 写代码前先画 ER 与索引图,确认聚合字段是否走索引;否则数据量上涨后接口必塌
  4. 聚合结果字典是「契约」,自定义 key 时要保持命名统一,如 total_amountavg_amount,方便上游缓存与前端解析
  5. 单元测试里用 TransactionTestCase 造空表、单条、多条三种场景,断言 None / 0 / 正常值,防止空表抛 KeyError
  6. 高并发场景记得加缓存,但缓存 Key 要带「业务维度 + 时间窗口」,避免脏读
  7. 养成查看 queryset.queryexplain 的习惯,确认聚合语句是否走最优索引;性能调优从 SQL 开始,而不是加机器

把「计算下推、索引先行、缓存兜底」三条思想固化到日常开发,可在需求方不断追加统计维度时,仍保持接口性能与代码可维护性的双赢。

5. 程序员面试题

简单题

Q: Django 中统计所有文章数量的最佳聚合语句是?

A:

python 复制代码
from django.db.models import Count
Article.objects.aggregate(cnt=Count('id'))['cnt']

中等题

  1. 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')
)
  1. Q: aggregateannotate 有何区别?请给出代码示例。
    A:
    aggregate 返回整个 QuerySet 的一个汇总字典;annotate 先 GROUP BY 再对每行附加聚合值。
python 复制代码
# 总销售额
Order.objects.aggregate(total=Sum('amount'))

# 每个用户的销售额
User.objects.annotate(total=Sum('order__amount'))

高难度题

  1. 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 刷新缓存,防止缓存击穿
  • 若仍慢,采用「日汇总表」+ 触发器实时更新,接口直接查汇总表
  1. 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. 知识点详解

  1. 创建应用
    python manage.py startapp box 生成目录后,务必在 INSTALLED_APPS 列表中追加 'box', 否则迁移不会生成表。

  2. 声明一对多关系

    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()
  3. 迁移

    bash 复制代码
    python manage.py makemigrations
    python manage.py migrate

    若提示 "no changes detected",99% 是忘记注册应用。

  4. 注册后台

    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']

    注册后才能在后台快速录入测试数据,验证关联是否生效。

  5. 录入数据技巧

    -先在 Book 表新增 3 本书;

    -新增 Role 时,外键下拉若显示"Book object(1)" 极不友好,解决方式就是给模型加 __str__,保存后刷新即可看到书名。

3. 章节总结

本节课完成了"图书-人物"一对多场景的数据层与后台层搭建:

  1. 建应用 → 2. 写模型(外键+on_delete+str ) → 3. 迁移 → 4. 注册 admin → 5. 造数据。
    只有数据正确落地,下一节才能放心演示 book.role_set.all()Role.objects.filter(book__title__contains='天龙') 等关联查询。

4. 知识点补充

  1. on_delete 其余选项:PROTECT、SET_NULL、SET_DEFAULT、DO_NOTHING 使用场景。
  2. db_index=True 对外键加索引,提升关联查询性能。
  3. select_related('book') 一次 SQL 把外键 JOIN 回来,避免 N+1。
  4. 多对多 ManyToManyField 与 through 自定义中间表。
  5. 后台 list_filter 按外键字段过滤,需用 ('book', admin.RelatedOnlyFieldListFilter)

最佳实践(>300 字)

生产环境写入测试数据时,不要手工在后台一条条点,而是编写"数据迁移脚本"或 Django-admin 命令,一方面可重复执行,另一方面能把初始数据纳入版本控制。步骤:

  1. 创建 box/management/commands/init_novel.py
  2. 继承 BaseCommand,在 handle() 里用 bulk_create 批量插入 Book 与 Role;
  3. 把脚本放在 CI/CD 流程,容器启动时自动 python manage.py init_novel
  4. 若数据量大,使用 iterator() 分批读取,防止内存暴涨;
  5. 对外键字段先插主表再插子表,保证主键已存在;
  6. 脚本幂等:先 get_or_create 判断,防止重复执行导致唯一键冲突。
    这样新同事一键即可拥有完整测试数据集,再也不用对着空白页面手动录入,极大提升开发效率。

编程思想指导(>300 字)

"先数据,后查询"是 ORM 学习的第一性原理。很多初学者急着写 .filter(),结果字段没加索引、外键忘写 on_delete、模型没有 __str__,导致后台无法调试、查询报错、性能爆炸。正确思路:

  1. 业务关系 → UML 草图 → 模型字段+关联类型;
  2. 每写一行模型代码,立刻问自己"以后我要怎么查?"------如果会按书名查人物,就在 Role 建外键并给 book.title 加索引;
  3. 把 Django Admin 当成"可视化单元测试",任何关联必须先能在后台顺利录入、展示,再写查询代码;
  4. 利用 related_name 把"反向查询"语义化,让代码自解释:book.roles.all()book.role_set.all() 更易读;
  5. 写完查询后一定打开 DEBUG=True 的 SQL 面板,看是否产生 N+1,及时 select_related/prefetch_related
  6. 模型即接口文档,保持字段命名与产品术语一致,减少前后端歧义。
    坚持"数据层先行 + 可视化验证 + 性能观察"三步走,后续无论写 REST 接口还是 GraphQL,都能复用同一套模型思维,少走弯路。

5. 程序员面试题

简单题

  1. 写出在 Django 中声明"图书-人物"一对多模型的完整代码,并指出外键必须携带的参数。
    答:见上 models.py 片段,必须写 on_delete

中等难度

  1. 后台外键下拉显示"Book object(1)"如何改成可读的"书名"?

    答:给 Book 模型增加 __str__ 方法,返回 self.title

  2. 简述 makemigrationsmigrate 的区别。

    答:makemigrations 根据模型变更生成迁移文件;migrate 把迁移文件翻译成 SQL 并在数据库执行。

高难度

  1. 当删除一本图书时,若要求关联人物不被物理删除而是把外键置空,模型应如何写?

    答:book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True)

  2. 已知需要按"图书阅读量降序取出每本书的前 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 把"一端"与"多端"数据一次性取出。

掌握两种入口:

  1. 从模型实例(对象)出发
  2. 从模型类(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

    sql 复制代码
    SELECT * 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 打回
  • 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
  • 默认 hero_set 可改名:
python 复制代码
class Hero(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE,
                             related_name='heros')
# 使用
book.heros.all()
  • 面试加分:若设置 related_name='+' 会禁用反向关联,提高封装性

3. 章节总结

  1. 一到多:obj.小写_set
  2. 多到一:obj.外键字段
  3. 过滤:小写__字段 双下划线
  4. 性能:select_related(单端)、prefetch_related(多端)
  5. 可读性:related_name 自定义反向名

4. 知识点补充 & 最佳实践

4.1 补充知识

  1. QuerySet.select_for_update():行级锁,金融支付常用
  2. exists()count() 更快判断有无记录
  3. only() / defer() 指定加载列,减少 IO
  4. through 模型:自定义多对多中间表,可存额外字段(加入时间、角色)
  5. 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_relatedprefetch_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 的双向关联查询写法:

  1. 已知"一"查"多"------由书取全部人物;
  2. 已知"多"查"一"------由人物取所属书籍;
  3. 通过模型类 直接做跨表过滤,避免先查对象再导航的低效代码。
    掌握这些写法,可一次性完成 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. 章节总结

  1. 会用 __ 双下划线就能在 Django 里写任意方向的 Join。
  2. 正向查"多"用 xxx_setrelated_name;反向查"一"直接取外键字段。
  3. 跨表过滤统一在 filter() 里写关联小写__字段__运算符,避免先拿对象再遍历。
  4. 性能牢记 select_related(一对一/多对一)与 prefetch_related(一对多/多对多)。

4. 知识点补充

4.1 额外 5 个高频扩展

  1. 自关联外键:如员工上下级 ForeignKey('self'),查询用 __parent__name
  2. 多对多:Through 模型自定义字段,查询方式与一对多完全一致。
  3. 反向命名冲突:同一模型被两个外键指向,需显式定义 related_name='a_set'/'b_set'
  4. 跨表更新:update(book=new_book) 可一次性改多条,不会调用 save() 信号。
  5. 原生 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)

  • 单条

    python 复制代码
    book = Book.objects.create(title="神雕", read_count=0, comment_count=0)
    # 返回值即是已保存的实例,自带 id
  • 批量

    python 复制代码
    Book.objects.bulk_create([
        Book(title="倚天", read_count=0),
        Book(title="笑傲", read_count=0)
    ])  # 1 条 SQL,O(1) 次往返

2.2 删除(Delete)

  • 单条

    python 复制代码
    people = People.objects.get(name="韦小宝")
    people.delete()          # 返回 (deleted_count, deleted_dict)
  • 批量

    python 复制代码
    People.objects.filter(read_count=0).delete()
  • 软删除(推荐)

    模型加 is_deleted = models.BooleanField(default=False)

    重写 delete() 方法改为更新 is_deleted=True,查询统一加 .filter(is_deleted=False)

2.3 修改(Update)

  • 单条先查后改

    python 复制代码
    p = People.objects.get(id=3)
    p.name = "扫地僧"
    p.save()                 # 会触发 pre/post_save 信号
  • 一条 SQL 完成(避免竞态)

    python 复制代码
    People.objects.filter(id=3).update(name="扫地僧")
  • 差异更新(仅改动的字段)

    python 复制代码
    p.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。牢记:

  1. 新增可 bulk_create
  2. 删除推荐软删除
  3. 更新用 update_fields 减少锁时间
  4. 查询用 select/prefetch_related 防 N+1

4. 知识点补充

  1. bulk_update:Django 2.2+ 提供,批量更新已存在对象,比逐条 save() 快 5~10 倍
  2. transaction.atomic:把多条 ORM 操作包成事务,要么全成功要么全回滚
  3. QuerySet.explain():打印 MySQL/PostgreSQL 执行计划,调优索引
  4. only/defer:只取部分字段,减少 IO;与 values() 区别是仍返回模型对象
  5. 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 字)

  1. "表驱动"思维:把重复 if/else 转换成数据+配置。例如状态机、权限位,用模型字段存码值,配合 TextChoices 枚举,代码即文档
  2. "资源即对象":Django 模型=业务实体,所有操作都通过方法封装进模型/管理器,如 Book.best_sellers()People.soft_delete(),Controller 只负责调度,符合单一职责
  3. "SQL 先行":复杂查询先在数据库 CLI 跑通,再倒翻译成 ORM;用 QuerySet.query 属性打印 SQL,确保命中索引
  4. "失败早暴露":利用 Django 校验层------clean()validators,在模型层就抛 ValidationError,避免脏数据到 DB
  5. "性能靠度量":每写一段数据访问,都加 assertNumQueries 单元测试;生产环境开启 connection.queries 日志,>N 条即报警
    坚持以上思想,ORM 不再是"黑盒",而是可预测、可单元测试、可水平扩展的业务基石

5. 程序员面试题

【简单】

  1. 写一条 Django ORM 语句,向 Book 表插入 title="三体" 的记录
    答案Book.objects.create(title="三体")

【中等】

  1. 解释 select_relatedprefetch_related 的区别,并给出各自适用场景
    答案select_related 做 SQL JOIN,适用于多对一/一对一;prefetch_related 做 Python 端二次查询+内存拼接,适用于多对多/反向一对多,可避免 N+1
  2. 如何防止"丢失更新"问题?至少给出两种 Django 实现
    答案
    • F() 表达式原子更新
    • select_for_update() 行级锁
    • 在模型加 version = IntegerField(default=0) 乐观锁,更新时 version+1 并检查 where 条件

【困难】

  1. 现有 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

  2. 设计一个"软删除"管理器,使得 People.objects.all() 默认过滤掉已删除数据,但仍提供接口拿到全量数据
    答案

    python 复制代码
    class 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 机制)

  1. 项目级 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')),   # 关键行
]
  1. 应用级 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'),
]
  1. 匹配过程
    浏览器访问 /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/ 目录,防止重名冲突。
  • 模板查找顺序:DIRSAPP_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. 章节总结

  1. 视图函数 = 接收 request → 返回 HttpResponse。
  2. 路由分层 = 项目 urls 用 include 把"前缀"分发给各应用,应用 urls 再细化规则。
  3. 模板路径 = settings 里配置 DIRS,否则 render() 找不到全局模板。
  4. 新增应用三件套:注册 → 建模型 → 迁移。
  5. 访问链路:浏览器 → 项目 urls 截断 → 应用 urls 匹配 → 视图 → 响应。

4. 知识点补充

4.1 必会 5 个扩展点

  1. path 转换器:int:pk、slug:title 自动捕获并校验类型。
  2. re_path 正则:兼容旧版 (?P<name>\d+),适合复杂规则。
  3. app_name 命名空间:模板 {% url 'news:login' %} 与 Python reverse('news:login') 必备。
  4. 404/500 全局处理:在根 urls 加 handler404 = 'news.views.page_not_found'
  5. 中间件顺序:request 自上而下,response 自下而上,自定义中间件一定测顺序。

4.2 最佳实践:多人协作中大型项目路由规范

当项目包含 20+ 应用、团队并行开发时,路由管理直接影响合并冲突与线上事故率。建议采用"三级路由 + 命名空间 + 版本前缀"方案:

  1. 一级路由(项目 urls)

    只放"业务线前缀"与"版本前缀",如
    path('api/v1/community/', include('community.urls'))

    禁止出现任何具体视图,确保后续可灰度升级 v2。

  2. 二级路由(应用 urls)

    每个应用自建 urls/api.pyurls/admin.pyurls/mpa.py,把"接口/后台/前台"彻底隔离;

    统一加 app_name = 'community' 命名空间,防止反向解析重名。

  3. 三级路由(子业务模块)

    当应用再拆子模块时,可用 include() 再次下发,例如
    path('posts/', include('community.urls.posts'))

    此时文件名即模块名,CRUD 路由一眼定位。

  4. 路由文档化

    在应用根目录维护 ROUTING.md,用表格列出"URL → 视图 → 权限 → 备注",合并请求前强制更新,方便测试与运维 grep。

  5. 自动化检测

    写单元测试遍历 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. 程序员面试题

【简单】

  1. 写出 Django 视图函数的最小定义,并说明返回值类型。
    答案:
python 复制代码
from django.http import HttpResponse
def demo(request):
    return HttpResponse('ok')

必须返回 HttpResponse 或其子类。


【中等】

  1. 解释 path('blog/', include('blog.urls')) 的匹配与"截断"过程。
    答案:

    访问 /blog/article/1/ 时,Django 先匹配 blog/,把剩余 article/1/ 截断后转发给 blog.urls 继续匹配;blog.urls 里再写 path('article/<int:pk>/', ...) 即可命中。

  2. 反向解析时为什么要加 app_name?给出模板与 Python 两种写法。
    答案:

    防止同名路由冲突。模板:<a href="{% url 'blog:detail' pk=1 %}">;Python:reverse('blog:detail', kwargs={'pk': 1})


【困难】

  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 类实现逻辑复用。

  1. 线上出现 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 传入视图
]
  • 内置转换器:str int slug uuid path
  • 转换器失败 → 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=videorequest.GET.get('q') 获取
  • 不属于路由层,不受 urlpatterns 匹配

2.6 include() 拆分路由

python 复制代码
# 主 urls.py
urlpatterns = [
    path('cms/', include('cms.urls')),
    path('pay/', include('pay.urls')),
]
  • 模块级解耦,大型项目必用

3. 章节总结

  1. 请求 → Web 服务器 → Django → ROOT_URLCONF → 顺序匹配 urlpatterns
  2. path() 适合静态、语义化 URL;re_path() 提供正则灵活性
  3. 捕获分组决定视图参数:位置参数按顺序,命名参数按名字;二者不可混用
  4. 查询字符串走 request.GET,与路由无关
  5. 多级路由用 include() 拆分,保持 URL 与业务模块 1:1 映射

4. 知识点补充

4.1 补充知识

  1. 自定义 Path Converter

    实现 class FourDigitConv: regex = r'\d{4}' 并注册,可 path('archive/<yyyy:year>/')

  2. URL 命名与反向解析
    path('login/', views.login, name='signin'){% url 'signin' %}reverse('signin'),避免硬编码

  3. app_name 空间

    include 时指定 namespace='pay',模板 {% url 'pay:checkout' %} 防止不同 App 同名冲突

  4. 尾部斜杠策略
    APPEND_SLASH=True 自动 301 补 /,SEO 更友好;REST API 常关闭

  5. 性能注意

    urlpatterns 顺序影响性能,高频接口放前面;正则比转换器慢 3~5 倍,能用 path 就别 re_path

4.2 最佳实践(≥300 字)

"先语义、后性能、再兼容" 是设计 Django URL 的三段论。

  1. 语义化 :对人和搜索引擎友好。商品详情用 /product/<slug:slug>-<int:pk>/ 而非 /p/123,既含关键词又保证主键唯一。
  2. 版本化 :对外 API 必须带版本,如 /api/v2/orders/。版本变动时旧链接仍然可用,通过 include() 把不同版本路由指向不同视图模块,实现 零侵入式升级
  3. 参数最小化:只暴露必要变量,隐藏内部主键可用 Hashids 库把整数混淆成短字符串,防止爬虫遍历。
  4. 统一尾部斜杠 :团队约定全部带 /,Django 自动跳转,减少 404 日志。
  5. 自动化测试 :使用 django.test.Client 针对每个 URL 模式写 resolvereverse 双向测试,确保改名或重构后不会 404。
  6. 监控 :线上开启 django-request-logging,对匹配失败路径做聚类,及时发现黑客扫描或拼写错误导致的 404。
    遵循以上 6 条,URL 层即可在长期迭代中保持 高可读、高稳定、高 SEO

4.3 编程思想指导(≥300 字)

路由层是 "Web 系统的门牌号",其设计思想直接影响架构可扩展性。

  1. 开闭原则 :对新增开放、对修改关闭。用 include() 给每个业务域独立 urls.py,新增模块无需改动根路由文件。
  2. 单一职责 :一个视图只干一件事,URL 段也保持 "名词级" 粒度,如 /account/reset//account/profile/,拒绝把不同动作堆到同一条路由再用 ?action= 区分。
  3. 显式优于隐式 :命名参数优先于位置参数,让调用方(模板、前端、API 网关)通过 reverse('pay:checkout', kwargs={'order_id': 123}) 一眼看清含义。
  4. 契约式编程 :路由正则就是 "对外契约" ,一旦上线不得随意收紧(否则老链接 404),版本号或自定义 Converter 给契约增加 "兼容缓冲区"
  5. 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,如何在视图里分别拿到 1232 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.pyapi/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'

  1. 构造新路径 new_path = path + '/',返回 HttpResponsePermanentRedirect(new_path)

  2. settings.MIDDLEWARE 最前插入该中间件,保证比 CommonMiddleware 先执行;

  3. 单元测试覆盖带参、POST 请求,确保仅 301 一次;

  4. 灰度观察 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. 知识点详解

  1. 关闭 DEBUG

    • settings.pyDEBUG = False
    • 必须同时给出 ALLOWED_HOSTS = ['*'](演示用)或真实域名,否则 Django 拒绝服务
  2. 404 触发时机

    • URLconf 无匹配 → 404
    • 模板/静态文件找不到不会触发 404,由 Web 服务器(Nginx)处理
  3. 500 触发时机

    • 视图函数或中间件未捕获异常 → Django 捕获后返回 500
    • 若再次渲染 500.html 又出错 → Django 退回到内置硬编码页面,不再泄露栈
  4. 模板放置

    • 放在 TEMPLATES[0]['DIRS'] 指定的目录根下即可,文件名必须叫 404.html / 500.html
    • 生产环境推荐放在模板主题目录 便于复用,例如 templates/theme/error/404.html,再在视图里 render(request, 'theme/error/404.html', status=404)
  5. 参数混用警告

    • 同一条路由里既写 <int:uid> 又用 path('user/<int:uid>/', views.detail, name='user-detail') 没问题
    • 不要 前面用未命名组 (?P<uid>\d+) 后面又用 <int:uid>,会让正则与新版语法冲突,匹配失败率陡增

3. 章节总结

  1. 上线三步曲:关 DEBUG → 配 ALLOWED_HOSTS → 放 404.html / 500.html
  2. 文件名固定,放在模板根目录即可生效;若想自定义路径,可在视图里手动 render 并指定 status
  3. 视图代码抛异常 → Django 自动返回 500,无需手写 try/except
  4. 同一路由配置里避免混用"位置参数"与"关键字参数",降低正则冲突概率

4. 知识点补充

  1. 403、400 错误页

    Django 同样支持 403.html / 400.html,触发场景:CSRF 验证失败、SuspiciousOperation 等

  2. Sentry 集成

    生产环境建议接入 Sentry,异常日志自动上报,错误页仍对用户友好

  3. Nginx 错误页兜底

    即使 Django 未启动,Nginx 也能捕获 502/504,因此双层错误页(Nginx + Django)是标准做法

  4. 模板继承

    404/500 页可继承 base.html,但必须保证父模板不依赖任何可能出错的上下文变量,否则 500 页会二次爆炸

  5. 单元测试

    使用 django.test.SimpleTestCase.assertRaisesMessageoverride_settings(DEBUG=False)自动化验证错误页是否返回 200 且包含友好文案


最佳实践(≥300 字)

目标 :让错误页在任何极端情况下都能渲染成功 ,同时给运维留下足够排障信息。
步骤

  1. 统一错误模板目录
    templates/error/ 下放 400/403/404/500.html,全部继承同一套最小化 error_base.html,仅保留品牌 Logo、一句话提示、返回首页按钮,不依赖任何模型查询

  2. 静态资源离线化

    错误页用到的 CSS/图片全部内联 (base64 或 <style> 标签),防止静态服务器挂掉时样式丢失

  3. 上下文安全降级

    settings.py 新建上下文处理器 error_context,只返回 {'STATIC_URL': '/static/'}杜绝使用可能抛异常的函数

  4. 日志双通道

    • Django logging 配置把 500 异常写本地文件 /var/log/django/error.log
    • 同时发 UDP 到 Sentry,保留完整栈供开发排查,但前端用户永远看不到
  5. 自动化回归

    CI 阶段加一条:python manage.py test --settings=settings_ci apps.core.tests.test_error_pages

    django.test.Client 分别请求 / definitely_not_exist人为抛出 RuntimeError 的视图,断言:

    • 状态码 404/500
    • 响应体包含"返回首页"
    • 响应体不包含 Traceback 关键字
      一旦测试失败即阻断上线
  6. 灰度开关

    通过环境变量 FRIENDLY_ERROR_PAGE=on/off 控制是否启用自定义模板,方便线上紧急回退

按以上六步落地,可保证用户体验、安全、排障效率三者兼得


编程思想指导(≥300 字)

"错误也是功能" ------把异常处理当成显性需求 写进 Backlog,而不是事后补丁。
防御式思维三层模型

  1. 用户层

    任何异常都要收敛到统一出口 :Django 的 handler404 / handler500 或 Spring 的 @ControllerAdvice。禁止裸抛栈,文案统一、风格统一、跳转统一

  2. 服务层

    业务代码只捕获可预期异常 (数据库唯一键冲突、第三方支付超时),记录后转译为用户可读错误码不可预期异常 全部上抛,由全局兜底页处理。

    规则:只有知道怎么恢复才捕获,否则让它炸

  3. 运维层

    日志 = 现场照片,必须包含 request_id、user_id、SQL、堆栈、时间戳 ;同时用 APM(SkyWalking、Jaeger)做链路追踪,把"用户看到的错误"与"系统内部异常"一键关联

进阶:混沌工程 ------主动注入异常(kill 数据库连接、打满 CPU),验证错误页是否仍能渲染、告警是否及时。

最终形成**"异常→日志→告警→复盘→自动化用例"闭环,才能把"错误"从不可控变成可度量、可改进的稳定性功能**


5. 程序员面试题

简单题

  1. 问:Django 生产环境为什么要设置 DEBUG=False
    答:防止把异常栈、配置信息泄露给终端用户,同时激活自定义 404/500 错误页

中等难度题

  1. 问:访问不存在的 URL,Django 返回 404 的完整流程?

    答:

    • URLconf 无匹配 → django.core.urlresolvers.resolveResolver404
    • Django 调用 django.views.defaults.page_not_found
    • 若存在 templates/404.html 则渲染并返回 404 状态;否则返回内置硬编码页面
  2. 问:500 错误页二次渲染失败会怎样?

    答:Django 退回到内置硬编码 500 页面 ,不再尝试渲染任何模板,确保绝不再次泄露异常信息


高难度题

  1. 问:如何针对 AJAX 请求 返回 JSON 格式的 404/500,而非 HTML?

    答:

    • 在自定义 handler404 / handler500 视图里判断 request.headers.get('x-requested-with') == 'XMLHttpRequest'
    • 返回 JsonResponse({'code': 404, 'msg': '资源不存在'}, status=404)
    • 同时保留 templates/404.html 供普通页面请求降级使用
  2. 问:高并发场景下,视图因数据库连接池耗尽 频繁 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 的 WSGIHandlerenviron 实例化 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") → 取最后一个值,无则返回 None
    • q["age"] → 同上,但 KeyError
    • q.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 文件上传

  1. HTML 必须 enctype="multipart/form-data"
  2. 视图读取:
python 复制代码
file = request.FILES["avatar"]   # InMemoryUploadedFile
dest = open("media/avatar.jpg", "wb+")
for chunk in file.chunks():
    dest.write(chunk)
dest.close()
  1. 大文件用 file.chunks() 避免一次性加载到内存

2.6 请求头读取规则

  • 全部放在 request.META
  • 浏览器头小写+下划线+大写,加前缀 HTTP_,如:
    • Accept-LanguageHTTP_ACCEPT_LANGUAGE
    • Content-TypeCONTENT_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. 章节总结

  1. Django 把一次 HTTP 请求封装成 HttpRequest 实例,视图第一个参数即 request
  2. 查询参数用 request.GET,表单/体参数用 request.POST,上传文件用 request.FILES
  3. request.GET/POST 是 QueryDict,支持一键多值,用 .getlist() 取列表
  4. 请求头统一在 request.META,自定义头加 HTTP_ 前缀
  5. 调试时打印 request.method / path / GET / POST / body 可快速定位问题
  6. 面试常考 GET/POST 区别、CSRF 流程、文件上传步骤、QueryDict 与 dict 差异

4. 知识点补充

4.1 补充 5 个高频扩展点

  1. HttpRequest.get_host():返回 Host 头,防 XSS 中做 Host 校验
  2. HttpRequest.build_absolute_uri(location):拼完整 URL,常用于分页、邮件
  3. request.content_type / request.encoding:解析非 JSON 或指定编码
  4. request.read(1024):底层流式读取,用于自定义协议转发
  5. request.META["REMOTE_ADDR"] 取真实 IP,但注意反向代理需配置 X-Forwarded-For

4.2 最佳实践:登录接口参数校验与日志(≥300 字)

实际开发中,登录接口往往同时支持 JSON POSTform 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/jsonapplication/x-www-form-urlencoded,前端无需改动
  • 登录成功后把 uid 写入 session,Django 自动下发 sessionid Cookie;后续视图用 request.user 或自定义中间件即可识别用户

4.3 编程思想指导:以"请求对象"为中心的设计(≥300 字)

  1. 面向封装 :把 Web 层所有输入收敛到 request 对象,业务层只依赖"干净的数据结构"而非原始 HTTP,降低单元测试成本。
  2. 最小权限 :视图函数优先使用 request.POST.get()request.GET.getlist() 而不是直接读 request.body,避免解析副作用;对敏感接口统一加 @csrf_exempt 或自定义鉴权中间件,保持"默认安全"。
  3. 契约优先 :团队内部约定"查询用 GET、提交用 POST、REST 更新用 PUT/PATCH、删除用 DELETE",并在代码里用 request.method 做断言,拒绝非法动词,降低调试难度。
  4. 流式与内存平衡 :小文件直接 request.FILES["f"].read(),大文件必须 chunks();JSON 小于 1 MB 可直接 request.body,大于 1 MB 考虑客户端分片或直传 OSS,避免占用 Python 进程内存。
  5. 可观测 :所有视图入口打印 request.get_full_path()request.content_type,并记录响应时间;线上出错时结合 Sentry 的 "Request" 面板可秒级还原参数、请求头、Cookie,极大缩短排障时间。

把以上思想固化到脚手架:新建项目即带统一异常处理、IP 获取、日志格式、限流中间件,后续业务开发者只需关注"从 request 中取业务参数",就能写出高可测、高可维护、面试能讲清原理的 Django 代码。


5. 程序员面试题

简单题

  1. 在 Django 视图里如何取得查询参数 page 的值?请写出两种写法。
    答案
python 复制代码
page = request.GET.get("page")          # 方式1
page = request.GET["page"] if "page" in request.GET else None  # 方式2

中等难度

  1. 当 HTML 表单存在同名多选框 <input type="checkbox" name="tag" value="python">,后端如何拿到全部已选项?
    答案
python 复制代码
tags = request.POST.getlist("tag")   # 返回列表,如 ["python", "django"]
  1. 请解释 request.POSTrequest.body 的区别,并说明在什么情况下 request.POST 为空。
    答案
  • request.POST 只封装 application/x-www-form-urlencodedmultipart/form-data 的解析结果
  • request.body 是原始字节流,任何请求方式都有值
  • Content-Typeapplication/jsontext/plain 时,Django 不会解析表单,故 request.POST 为空

高难度

  1. 线上环境通过 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。

  1. 实现一个装饰器,要求:
    • 只允许 AJAX 请求访问(X-Requested-With: XMLHttpRequest
    • 只接受 application/json 格式
    • 失败时返回 406 Not Acceptable
      请给出完整代码。
      答案
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 服务器异常 ❌(被收录后影响排名)
  1. 写入:response.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite=None)
    • max_age 秒级有效期;expiresdatetime 对象,二者选其一
    • 不指定时=会话 Cookie,关闭浏览器即失效
  2. 读取:request.COOKIES.get('token')
  3. 删除:response.delete_cookie(key, path='/', domain=None)
    注意:删除时必须与写入时的 path/domain 完全一致,否则浏览器忽略

2.5 安全加固

  • httponly=True 禁止 JS 读取,防 XSS
  • secure=True 仅 HTTPS 传输
  • samesite='Strict' 彻底阻止 CSRF POST 请求携带 Cookie;'Lax' 允许顶级导航 GET
  • 敏感数据不要直接放 Cookie,可存 sessionidJWT 签名串

3. 章节总结

视图必须返回 HttpResponse;render 只是帮你套了模板。掌握 statuscontent_typeset_cookie/delete_cookie 就能自由控制"返回什么、怎么存、如何删"。Cookie 是前端状态的第一站,合理设置安全属性才能抵御 XSS & CSRF。


4. 知识点补充

4.1 额外 5 个高频考点

  1. JsonResponse 继承自 HttpResponse,自动序列化字典+设置 content_type=application/json
  2. StreamingHttpResponse 用于大文件流式下载,节省内存
  3. FileResponse 基于 StreamingHttpResponse,封装了 Content-Disposition: attachment
  4. HttpResponseRedirectredirect() 快捷函数,内部状态码 302,可改 permanent=True→301
  5. set_signed_cookie(key, value, salt='') 带签名防篡改,读取用 get_signed_cookie

4.2 最佳实践:登录态"双 Token" 方案(Cookie + Redis)

背景:纯 Cookie 存 JWT 易被 XSS 窃取,纯 Session 又难做分布式。

方案:

  1. 登录成功后,服务端生成两个串:
    • access_token:JWT,有效期 15 min,写入 HttpOnly / Secure / SameSite=Strict Cookie
    • refresh_token:随机 32 位字符串,有效期 7 天,写入 Secure Cookie(HttpOnly=true
  2. refresh_token 作为 key,用户 ID、权限、过期时间序列化后存入 Redis,并设置 7 天 TTL
  3. 每次请求带 access_token,网关层直接验签,无需查 Redis,性能高
  4. access_token 过期,浏览器调用 /api/token/refresh,服务端校验 refresh_token 是否在 Redis 中:
    • 存在 → 颁发新 access_token 并更新 Redis TTL
    • 不存在 → 返回 401,前端跳转登录
  5. 登出时,后端 delete_cookie 双 Token,同时 DEL refresh_token 清 Redis;即使攻击者窃取了本地 Cookie,也无法在服务端换到新 Token

优点:

  • 验签无 I/O,QPS 高
  • 泄露后可通过 Redis 一键吊销
  • 兼容 SSR 场景,Cookie 自动携带,前端零感知

4.3 编程思想指导:把"响应"当成"消息"

  1. 单一职责:视图函数只负责"业务→消息",渲染或序列化交给专用组件(模板、序列化器)
  2. 不可变消息:一旦 HttpResponse 对象 return,就不可再修改;需要中间件层级加工时,使用 response.setdefault() 而非直接赋值
  3. 契约优先:先写 content_type & status,再填 content;对外接口文档即代码
  4. 最小暴露:Cookie 只存"指针"(sessionid / token),不存实体数据,减少网络往返与隐私风险
  5. 失败快速:异常分支尽早 return HttpResponse(status=4xx),避免深层嵌套 if-else,提高可读性

5. 程序员面试题

【简单】

  1. Django 视图函数能否直接 return "hello"?为什么?
    答:不能,必须返回 HttpResponse 或其子类,否则抛 ValueError

【中等】

  1. 如何让浏览器关闭后 Cookie 自动失效?
    答:调用 set_cookie 时不传 max_age 也不传 expires,生成会话 Cookie
  2. 前端 JS 无法读取 Cookie,可能后端做了哪些设置?
    答:httponly=True;也可能是 secure=True 且当前协议为 HTTP;或 samesite=Strict 导致跨站不携带

【困难】

  1. 描述一次"登录→颁发 Token→后续请求鉴权"的完整 Cookie 交互流程,并指出如何防止 CSRF
    答:登录后服务端 set_cookie(key='token', value=jwt, httponly=True, secure=True, samesite=Strict);后续浏览器同一站点请求自动带 Cookie;服务端在中间件校验 JWT。CSRF 防护:Strict 禁止跨站 POST 携带 Cookie,或配合 X-CSRFToken 双重验证
  2. 现网出现用户偶尔"登录态丢失",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 分钟讲透两条高频技能:

  1. 用 JsonResponse 优雅返回 JSON
  2. 用 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 种写法

  1. 直接返回字典
python 复制代码
return JsonResponse({'code': 0, 'msg': 'ok'})
  1. 返回列表,记得 safe=False
python 复制代码
return JsonResponse([1, 2, 3], safe=False)
  1. 自定义日期格式
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)
  1. 中文不乱码: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=Falseensure_ascii

重定向用 redirect() 最简洁,牢记 301/302 语义差异即可。


4. 知识点补充

  1. Django 的 DjangoJSONEncoder 可序列化 Decimal / UUID / datetime
  2. 当数据量 > 1 MB 时,建议开启 GZip 压缩:Content-Encoding: gzip
  3. 重定向路径以 / 开头会跳过 URLconf,直接按绝对路径解析
  4. 若需返回 201 Created,可继承 JsonResponse 并指定 status=201
  5. 前后端分离项目务必开启 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"看似只是调个函数,背后体现的是契约思想分层思想

  1. 契约优先:前后端先约定外层格式、字段含义、错误码区间,再写代码;契约一旦发布,向后兼容,杜绝"顺手改个字段名"的破坏行为。
  2. 分层隔离:视图层只负责"取参→调业务→拼数据→返回",不把 SQL 或第三方 SDK 异常直接抛给前端;通过标准响应包装,让前端拿到的是结构化错误,而非 500 堆栈。
  3. 最小惊讶:JsonResponse 默认 safe=True,防止开发者无意返回数组造成 JSON 劫持;理解框架设计意图,比死记硬背参数更重要。
  4. 性能与可维护权衡:小对象直接序列化,大列表(如导出 10 万行)应改用 StreamingHttpResponse 或分页,避免一次性吃掉内存。
  5. 测试即文档:给每个接口写 doctest / pytest,断言返回结构,比事后补 Swagger 更准确。

把"返回数据"当成对外发布的 API 合约,而不是临时补丁,你的代码将天然具备可扩展、可迁移、可回滚的能力。


5. 程序员面试题

【简单】

  1. JsonResponse 与 HttpResponse 在返回 JSON 时的核心区别是什么?
    答:JsonResponse 会自动把 Python 对象序列化成 JSON 字符串,并设置 Content-Type: application/json;HttpResponse 需要手动序列化并设置头。

【中等】

  1. 返回列表 [1,2,3] 时浏览器报 TypeError,如何解决?
    答:JsonResponse 默认 safe=True,只允许 dict,把 safe=False 即可。
  2. 登录成功后跳转到个人中心,请写出两种重定向代码。
    答:
python 复制代码
# 方法1
return HttpResponseRedirect('/user/profile/')
# 方法2(推荐)
return redirect('user:profile')   # 依赖 URL 别名

【困难】

  1. 构造一个返回 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
  1. 如何防止大型列表序列化导致内存暴涨?给出 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 执行链(面试常考)

  1. 请求进入 WSGI → Django 路由匹配 → MyView.as_view() 返回 _view 闭包
  2. _view 实例化 MyView() → 调用 dispatch()
  3. dispatch() 根据 request.method.lower() 找到 self.get/post...
  4. 执行对应方法 → 返回 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. 章节总结

  1. 类视图 = 把"请求方法"映射成"类方法"
  2. 路由绑定一定要用 as_view()
  3. 返回 JSON 列表记得 safe=False
  4. 先写基类/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 里。推荐三层:

  1. URL 层:只负责路由映射,不做任何业务判断。
  2. 视图层:只负责"请求分发 + 参数校验 + 响应格式化"。
  3. 服务层(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 时,先问自己三个问题:

  1. 哪些东西未来会变?------返回格式、权限、数据源。
  2. 变得频率多高?------返回格式常变,权限偶尔变,数据源很少变。
  3. 变得方向是什么?------可能从 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 模板语言三大语法

  1. 变量 {``{ }}
    调用对象属性或字典键,自动转义 HTML,防止 XSS。
    关闭转义:|safe{% autoescape off %}
  2. 标签 {% %}
    流程控制:for / if / elif / else / empty
    模板继承:{% extends 'base.html' %}{% block content %}{% endblock %}
  3. 过滤器 {``{ 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_exemptCSRF_USE_SESSIONS=True 配合 JWT。


3. 章节总结

  1. 先配 DIRS,再写 HTML,最后 render(request, 'xxx.html', context)
  2. 模板语言 = 变量 + 标签 + 过滤器;牢记 {% csrf_token %}
  3. 静态文件用 {% static %},上线务必 collectstatic
  4. 模型数据 → 视图查询 → 注入上下文 → 模板循环展示,是 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
    继承层级
  1. base.html 只放 <html><head><body> 框架与全局 CSS/JS;
  2. 每个业务模块再建 module_base.html,继承全局 base,再定义 {% block module_style %}
  3. 最终页面只关注自身内容,不重写公共代码。
    数据注入原则
    视图只查"必要字段",禁止 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()

相关推荐
yzx9910134 小时前
基于Django的智慧园区管理系统开发全解析
后端·python·django
lzptouch1 天前
Django项目
后端·python·django
Anson Jiang2 天前
PyTorch轻松实现CV模型:零基础到实战
pytorch·python·django·flask·python开发
Python私教4 天前
基于 Django 5 + DRF 构建博客系统后端接口(从建模到接口实现)
python·django·sqlite
IT学长编程4 天前
计算机毕业设计 基于Python的热门游戏推荐系统的设计与实现 Django 大数据毕业设计 Hadoop毕业设计选题【附源码+文档报告+安装调试】
大数据·python·django·毕业设计·课程设计·毕业论文
rexling14 天前
【玩转全栈】----Django基本配置和介绍
数据库·django·sqlite
IT学长编程5 天前
计算机毕业设计 基于Python的电商用户行为分析系统 Django 大数据毕业设计 Hadoop毕业设计选题【附源码+文档报告+安装调试】
大数据·hadoop·python·django·毕业设计·课程设计·电商用户行为分析系统
maotou5265 天前
dvadmin开发文档(第一版)
python·django
硬件人某某某6 天前
python基于卷积神经网络的桥梁裂缝检测系统(django),附可视化界面,源码
python·cnn·django