Python开发从入门到精通:Web框架Django高级应用实战 前言「从入门到精通」这个说法本身值得澄清精通不是某个可以打勾的里程碑而是一组具体能力的集合——知道 ORM 什么时候会偷偷发出额外的 SQL、会用事务保证一组写操作的原子性、能写中间件处理横切关注点、在上线前改掉那几个致命的默认配置。本文就把这几块挑出来讲它们才是「入门」和「能上生产」之间的分水岭。还有一个前提要摆正Django 的「高级」不体现在用了多少冷门 API而体现在知不知道默认行为的代价。比如ForeignKey的取值会触发额外查询、queryset的惰性求值会在循环里爆发成 N1、DEBUGTrue上线等于把内部信息送人。这些都是默认行为不写错代码也会踩。本文以 Django 5.x / 6.x 为背景示例基于一个博客模型。Django 6.x 要求 Python 3.12 及以上5.2 支持 3.10 起请按自己的 Django 版本核对 Python 版本。一、N1 查询性能问题里最常见的一种看这段代码逻辑上没问题但它会发多少次查询# 适用于 Django 5.x / 6.x# 假设 Post 有一个指向 Author 的外键 authorfor post in Post.objects.all(): # 第 1 次查询取出所有 Postprint(post.author.name) # 每循环一次再查一次 Author如果有 100 篇文章就会发出1 100 101次查询这就是N1 查询问题。它的可怕之处在于本地数据少时毫无异样上线数据一多就慢得莫名其妙。修法有两种对应两种关系形态# 外键 / 一对一用 select_related底层是 SQL JOIN一次查完for post in Post.objects.select_related(author):print(post.author.name) # 不再产生额外查询# 多对多 / 反向一对多用 prefetch_related单独发一条 IN 查询再在 Python 里拼for tag in Post.objects.prefetch_related(tags):print(tag.name)两者的机制不同别记混方法适用关系实现方式查询次数select_related外键、一对一SQL JOIN1 次prefetch_related多对多、反向关系额外一条查询 Python 拼接2 次select_related还能继续深入字段如select_related(author__profile)但每深一层 JOIN 都更贵不要滥用。另外两个省流的工具only(title)只取指定字段、defer(big_text)排除某些字段。当表里有大文本或 blob 字段而你只需要列表页的几个字段时它们能显著减少传输量。注意only之后如果又访问了没取回的字段Django 会再发一次查询去补反而更慢。想验证查询次数可以在测试里用assertNumQueries# 适用于 Django 5.x / 6.xfrom django.test import TestCaseclass QueryCountTests(TestCase):def test_list_view_query_count(self):with self.assertNumQueries(1):list(Post.objects.select_related(author))二、表达式、条件与聚合F()在数据库层面做运算避免「读出来改完再写回」的竞态。# 适用于 Django 5.x / 6.xfrom django.db.models import F# 计数器加一直接生成 UPDATE ... SET views views 1Post.objects.filter(pk1).update(viewsF(views) 1)对比一下错误写法先post Post.objects.get(pk1)、再post.views 1、再post.save()。这三步之间另一个请求可能也在做同样的事于是两次自增只生效一次。F()把运算交给数据库执行天然原子。Q()构造复杂的条件表达式。# 适用于 Django 5.x / 6.xfrom django.db.models import Q# 标题或正文中包含关键词Post.objects.filter(Q(title__icontainsdjango) | Q(body__icontainsdjango))Q对象之间可以用|或、与、~非组合。默认的多个关键字参数是「与」的关系一旦需要「或」就必须请出Q。annotate()与aggregate()分组聚合。# 适用于 Django 5.x / 6.xfrom django.db.models import Avg, Count# annotate给每一行附上聚合结果authors Author.objects.annotate(post_countCount(post))for a in authors:print(a.name, a.post_count)# aggregate对整个结果集算一个汇总值print(Post.objects.aggregate(avg_viewsAvg(views)))区别一句话记牢annotate是「每行都加一列」aggregate是「整张表算一个数」。顺带一提aggregate返回的是字典不是 QuerySet。三、事务一组写操作要么全成功要么全不做最常用的工具是transaction.atomic它既能当装饰器也能当上下文管理器# 适用于 Django 5.x / 6.xfrom django.db import transactiondef transfer(from_id, to_id, amount):with transaction.atomic(): # 进入事务Post.objects.filter(pkfrom_id).update(viewsF(views) - amount)Post.objects.filter(pkto_id).update(viewsF(views) amount)# 出块时提交块内抛异常则整体回滚要点atomic块可以嵌套。内层块默认用保存点savepoint实现内层失败可以只回滚内层。durableTrue参数用来强制某个块成为最外层事务如果它被嵌套在另一个 atomic 里会抛RuntimeError。还有一种更省心的做法把DATABASES里对应连接的ATOMIC_REQUESTS设为TrueDjango 会给每个请求自动套一个事务个别不想套事务的视图用transaction.non_atomic_requests排除。事务里有一个陷阱必须单独讲不要在事务未提交时就触发「外部副作用」。比如发邮件、调第三方接口、给消息队列发消息。因为事务有可能回滚邮件却已经发出去了无法撤回。正确做法是把这类动作交给on_commit()# 适用于 Django 5.x / 6.xfrom django.db import transactiondef create_post(...):with transaction.atomic():post Post.objects.create(...)# 只有事务真正提交后才执行回滚就不执行transaction.on_commit(lambda: send_notification(post.pk))四、中间件把横切逻辑挂到请求链上中间件middleware适合处理「每个请求都要过一遍」的事日志、耗时统计、给响应加头部、登录校验的补充逻辑。函数式写法最直观# 适用于 Django 5.x / 6.ximport timedef timing_middleware(get_response):# 这里在启动时执行一次def middleware(request):start time.perf_counter()response get_response(request) # 交给下一层response[X-Elapsed-Ms] f{(time.perf_counter() - start) * 1000:.1f}return responsereturn middleware也可以写成类实例可调用# 适用于 Django 5.x / 6.xclass TimingMiddleware:def __init__(self, get_response):self.get_response get_responsedef __call__(self, request):start time.perf_counter()response self.get_response(request)response[X-Elapsed-Ms] f{(time.perf_counter() - start) * 1000:.1f}return response官方文档的模型是「洋葱」请求自外向内穿过每一层响应再自内向外穿回来。所以在get_response之前写的代码在处理请求时执行之后写的代码在处理响应时执行。写完记得在settings.py的MIDDLEWARE列表里登记顺序有意义越靠前的越外层。五、类视图与自定义管理命令类视图CBV把常见的请求分发逻辑抽象成类方法get()处理 GET、post()处理 POST基类负责分发。# 适用于 Django 5.x / 6.xfrom django.views.generic import ListViewclass PostListView(ListView):model Posttemplate_name blog/post_list.htmlcontext_object_name postspaginate_by 20def get_queryset(self):# 用 select_related 避免列表页的 N1return Post.objects.select_related(author).order_by(-created)CBV 牺牲了一些直观性换来复用ListView自动帮你做分页、自动把结果放进上下文。代价是「这个名字的方法到底在哪被调用」变得不直观调试时要顺着继承链看。团队里最好约定用哪一种别混着来。自定义管理命令用来写运维脚本比如每天清理过期数据# blog/management/commands/cleanup_posts.pyimport datetimefrom django.core.management.base import BaseCommandfrom django.utils import timezonefrom blog.models import Postclass Command(BaseCommand):help 删除创建时间超过 30 天的草稿def add_arguments(self, parser):parser.add_argument(--days, typeint, default30)def handle(self, *args, **options):cutoff timezone.now() - datetime.timedelta(daysoptions[days])deleted, _ Post.objects.filter(created__ltcutoff).delete()self.stdout.write(self.style.SUCCESS(f已删除 {deleted} 条))注意目录必须是应用名/management/commands/文件名就是命令名之后用python manage.py cleanup_posts --days 7调用。这套结构是 Django 约定的路径写错命令就不会出现。六、上线检查清单项目开发时的值上线必须改成DEBUGTrueFalseALLOWED_HOSTS[]真实域名列表SECRET_KEY硬编码从环境变量读取数据库SQLite生产级数据库并列好连接参数静态文件runserver代劳collectstatic后由 Web 服务器托管服务器manage.py runserverWSGI/ASGI 服务器 反向代理官方文档的原话是永远不要把DEBUG True部署到生产环境。开启时报错页会显示详细追踪和大量环境信息等于把服务器内部结构展示给攻击者而且 Django 会记录执行过的每一条 SQL内存会快速上涨。另外DEBUG False时如果不设置ALLOWED_HOSTS所有请求都会收到 400。常见坑点1. 循环里访问外键写成 N1❌for p in Post.objects.all(): p.author.name✅Post.objects.select_related(author)。2. 把select_related用在多对多上❌select_related(tags)多对多关系报错或不生效。 ✅ 多对多、反向关系用prefetch_related。3. 用「读-改-写」自增计数❌p Post.objects.get(pk1); p.views 1; p.save()高并发下丢更新。 ✅Post.objects.filter(pk1).update(viewsF(views) 1)。4. 混淆annotate与aggregate❌ 以为annotate返回一个汇总数字。 ✅annotate给每行加列返回 QuerySetaggregate给整体算一个值返回字典。5. 在事务里发邮件/调外部接口❌ 事务内直接send_mail()回滚了邮件却已发出。 ✅ 用transaction.on_commit(lambda: ...)推迟到提交后。6.durableTrue用在嵌套的 atomic 里❌ 内层块写atomic(durableTrue)抛RuntimeError。 ✅durable只用于确保成为最外层的事务块。7. 中间件写了却忘了注册或顺序搞反❌ 中间件文件写好了MIDDLEWARE里没加毫无效果。 ✅ 加进MIDDLEWARE并注意「越靠前越外层」。8.DEBUG True或ALLOWED_HOSTS没配就上线❌ 生产保留DEBUGTrue或改成False却忘设ALLOWED_HOSTS全部请求 400。 ✅ 上线前逐项核对上面的清单。总结主题核心结论查询优化外键用select_related多对多用prefetch_related避免 N1原子更新自增类操作交给F()别用读-改-写聚合annotate逐行加列aggregate整体汇总事务transaction.atomic外部副作用交给on_commit中间件洋葱模型「请求前」与「响应后」的代码位置决定执行时机部署DEBUGFalse、ALLOWED_HOSTS配好、SECRET_KEY走环境变量从「会写 Django」到「能上生产」跨越的不是 API 数量而是对默认行为代价的敏感度每次循环取关联对象时想一下会不会 N1每次自增想一下会不会竞态每次上线前过一遍清单。把这些变成肌肉记忆才算真正走到了「精通」那一段路上。