又聊几件我在 Django 里用得很开心的事
文章摘要
Julia Evans 这篇短文延续了她最近的一段「奇怪旅程」:试着用一种 2010 年前后的方式做网站——有一个 SQL 数据库,在后端渲染 HTML。她坦白这条路对她并不「容易」:她没在 2000 或 2010 年代学过这种做法,要补的东西很多。
她先解释了动机。此前她有信心的工具箱是:静态站点生成器(比如她的博客)、带一点 JavaScript 的静态站(比如她的 SQL playground)、以及用 Lambda 或 Go 后端配简单 Vue.js 单页应用(比如 mess with dns)。这套前端为主的方案对极简应用很好用,但当她开始想做一个有很多不同页面(而不只是一个页面)的东西时,那些需要写大量前端代码的选项就没那么吸引人了,于是她转向后端。她还给出一个有意思的观察:写一个尽量少用 JS 的后端站点,和写一个尽量少做后端的单页应用,对她来说感觉是一样的——尽管两者看起来是对立的——因为两种情况下她都只是想把尽可能多的逻辑集中在一个地方。
她喜欢的第一件事是 query builder。 Django 里可以定义一个 QuerySet 类,把各种可能用到的 WHERE 条件写成方法。她的视图代码调用起来是这样的:Events.objects.approved().for_tab(tab).with_festivals(...).is_free(...).is_outdoors(...);定义端则是一个继承自 models.QuerySet 的类,每个方法返回一个 self.filter(...)。她说定义过滤器的语法不是她的最爱,但她大部分时间只是在使用这些方法,读起来非常舒服,这让她想去看看别的 query builder 库——「以前我想的是『我会 SQL,谁需要 query builder』,但这种结构确实让代码读起来很好。」
第二件是模板过滤器。 她列举了自己用到的几个:urlize 把纯文本 URL 转成链接、linebreaksbr 把换行转成 <br>、date 格式化日期、以及 json_script——它把一个 Python 字典自动转成 JSON 并以安全的方式插进 HTML 的 <script> 标签里。「这些单独看都是小事,但它们能直接拿来用,不知怎的就带来了很大的差别。」她最喜欢的是 querystring:它能生成一个「只改动一个参数」的当前查询串链接,比如链到前一天、或者把某个参数移除。
第三件是自动数据库迁移。 她说自己仍然非常喜欢 Django 的自动迁移:改一下 model 加个字段,Django 就自动生成迁移。项目至今做了 19 次迁移,而且大概还会更多。「随着我对问题的理解发生变化,能轻松改数据库,这对我来说是巨大的差别。」
然后是两个她遇到的问题。 其一是继承:Django 文档有时会建议用类视图和继承来组织代码,她有四个视图共享很多代码,试了一下,「我不喜欢用继承在视图间共享代码的体验」,于是换成了函数,直观得多。「我在 Python 里从来没有过好的继承体验,我想我不会再试了。」不过她不介意用继承来使用 Django 自己提供的接口。这一段她还加了一句元评论:她最近在练习用「THING 让我感觉不好,我更喜欢 OTHER THING」的方式表达编程观点——她链接的那篇文章说函数式视图是「正确方式」,而她并不关心是否「正确」,只是很高兴知道别人对继承有类似感受。
其二是性能,她坦承「我不知道该怎么思考 Django 的性能」。某个时刻 LLM 爬虫发现了她的站点,开始以每秒约 10 个请求的速度打过来;她把它们屏蔽了,但这让她开始想站点的容量到底是多少。用 ab -n 1000 -c 1 简单压测,一台约每月 10 美元的 VM 上大约只能扛 2-3 QPS。她列了一串还没想明白的问题:偶发流量高峰要不要能扩容?要不要把站点设计得更容易缓存(缓存真的很难做对)?Django 性能文档说 Jinja 模板更快,要不要换?文档还说 {% block %} 比 {% include %} 快,差别大吗、为什么?
她由此得出一个关于框架的教训:因为 Django 是个「框架(tm)」,很容易不小心配错。她在读性能文档时看到一句「启用 cached template loader 通常能极大改善性能」,而她做 CPU profiling 时正好注意到大量时间花在渲染模板上;点进去才发现这个 loader 本来就是默认开启的,是她之前折腾别的东西时不小心关掉了。打开模板缓存之后,站点轻松就能处理约 12 QPS 而不跑满 CPU。
最后她提炼了一条与常见建议相左的经验:人们总说「有性能问题就去查数据库查询,也许加个索引」,但她碰到的一系列性能问题(比如这次的模板缓存)都不是慢查询导致的,所以对她来说先跑 CPU profile 更有用——何况她用的是 SQLite,慢查询的问题本来也会出现在 CPU profile 里。
HN 评论精华
那个 2-3 QPS 的数字引爆了讨论区,这是整条帖子最大的争议点。
- echoangle(20 条子回复):「这个数字低得离谱,我觉得肯定还有别的问题。」
- shakna:「这些数字听起来……像单线程的。像是他们在用开发用的 runserver 而不是 uwsgi 或 gunicorn。」imperio59 说得更明确:「生产环境不要用开发 HTTP 服务器,用 gunicorn 或等价物。我在很强的机器上也遇到过同样低到不可思议的吞吐,就是因为 dev server 是单线程的、完全不做并发。」
- bigfatkitten 的挖苦最到位:「这个数字放在 25 年前、在一台 700MHz 奔腾 III 上跑 Perl CGI,我都会觉得失望。」
- sgt:「Django 绝对能超过 12 QPS。15 年前我们在非常基础的硬件上、渲染模板加半复杂 UI,就能做到远超这个。」
- lazyant 给了完整配方:正确的生产架构(nginx → gunicorn → Django,nginx 负责静态资源)、SQLite 合适的 PRAGMA、加上对通用内容的缓存,应该能把 RPS 提升一到两个数量级。gnz11 补充:把 gunicorn 放在 nginx 后面,静态资源全交给 nginx,加上合适的 Cache-Control 头,并且要理解和使用 Django 的页面缓存。
- stuaxo 给了最实用的一条建议:谈性能的第一件事应该是装上 Django debug toolbar,用它搞清楚视图和它触发的查询到底在干什么。
Django ORM 与迁移:最强的护城河,也是最大的槽点
- altbdoor(最高赞开场):「老牌 Django。用过一堆框架(tm),但没有哪个像 Django 这样挠到我的痒处。我至今觉得它的 ORM 和数据库迁移系统无可匹敌。」
- ranedk 贡献了本串最有想象力的用法:他从 0.95 版就在用 Django,「过去十年里,即使在 Go 或 Java 技术栈里,我仍然用 Django 来做 model 和迁移」——他有生成器,从 Django model 生成 Gorm DAO 或 Java Hibernate 类。有了 LLM 之后更方便:在 Django 里写好 model 和自定义 QuerySet,让 LLM 生成 Go 的 DAO 和 getter/setter,再拿 Django 生成的查询去验证完整性。「Atlas、sqlx、sqlc 以及所有 Go 里类 ORM 的东西,都做不到 Django 那样的迁移。」0xpgm 感叹自己从没这么干过但很有道理,「你还能白拿 Django admin 界面」。leetrout 说「我们这样的人有一打」,并顺带留下一句他愿意为之赴死的观点:Django 的 app 机制对大型内部/单一用途产品是反模式,因为外键会跨 app 边界,「没有哪个团队能自律到真的把 app 当成关系和约束的边界」。
- BrenBarn(24 条子回复)代表反方:「Django 那个双下划线的 filter 语法对我来说像指甲刮黑板。我觉得他们没有用运算符重载做出一门真正的查询表达式语言,简直不可思议。」ezst 补充:现在 Python 有了类型,这块本可以改进,过滤器可以是带字段自动补全的 lambda 谓词,就像 .NET、Scala 那样。
- dnautics:「自动迁移让我害怕。我用 Elixir/Ecto,就自己写迁移!」他还提出有时你想为同一张表定义多个 schema(一个精简的给菜单/下拉框用,一个完整的给 CRUD 用)。selcuka 回复说 Django 有 proxy model 正是干这个的。
- a_c 分享了运行一个有上千次迁移、几十个 app 的 Django 应用的真实经验,是全帖信息密度最高的一条:涉及 model 的测试很慢,因为它会跑完所有迁移;官方推荐的业务逻辑模式是 active record,干净场景下不错,但操作涉及多个 model 时就不太行;data migration 在需要把枚举型数据(比如货币列表)同步给所有队友时很好用;新成员很容易被诱惑去新建一个 Django app,因为那感觉像从零开始、不用管既有业务上下文,但设计 app 之间的概念边界非常重要——正因为开 app 太容易,所以经常(不总是)是错的;squash migration 工具因为使用率低而能力有限。mstaoru 补了个提速技巧:用
--no-migrations并把测试库设成内存数据库。
模板系统的老账
seanwilson(11 条子回复)系统地列出了 Django 默认模板语言的限制并追问是否仍然存在:不允许用括号辅助布尔表达式;不能做基本算术(可以 add 但不能乘,要靠 widthratio 之类的 hack);不能把表达式赋给变量,只能重复自己;不能把模板代码生成的 HTML 捕获进变量以实现 slot 式组件;不能给 model 方法传参。「我理解『模板不该包含复杂逻辑』这套哲学,但上面这些相当随意,而且导致更难维护的代码。加法可以但乘法不行?布尔逻辑可以但不能加括号?」leetrout 回答说 Jinja2 是可以直接换的模板后端,甚至可以两者同时跑(当然不能混用);mcrk 提醒迁移本身简单且文档完善,问题在于第三方 Django app(多为遗留的)可能不支持 Jinja,先确认兼容性。
跑题但精彩的 asyncio 讨论
CraigJPerry(21 条子回复)说:async Python 有太多走火装置,但它确实偷走了现代 Python Web 的时代精神;同步 Django 有很多可取之处——只要部署得当(worker、放在带缓存的反向代理后面),即使在压力下也很容易运维。它不适合长连接 WebSocket 之类,但对 CRUD 型或企业应用往往非常高效。他列举的 asyncio 陷阱包括:无界的 asyncio.Queue 导致缓冲膨胀、无限并发的 asyncio.gather 把出站 socket 甚至文件描述符耗尽、在事件循环里误用同步的 requests 阻塞住整个循环;以及最惊悚的一个——如果你忘了持有 asyncio.create_task() 的引用,那套机制里全是弱引用,你的 task 可能在生产环境里还没运行或完成就被垃圾回收掉了。loevborg 深有同感:「我也觉得 asyncio 是个大 hack……我读到你需要把 task 存进一个 set 才能防止它被意外取消时简直不敢相信。这东西怎么就成了 AI 后端的主力技术栈?它像 node,但更慢,而且因为同步 IO 更容易搞砸。」codethief 直接求他写篇博客。
「你该换个技术栈」派
- faangguyindia 详细讲了他基本全面转向 Go + SQLite 的理由:Python Django 太费资源(看内存占用),他的一个 Go 后端在 100 QPS 下瓶颈在 IO 而非 CPU;Go 写并发容易,十年前的代码今天还能编译。gls2ro 的反问很有力:「一个是编程语言加数据库服务器,另一个是全栈 Web 框架,这怎么比?(不用 LLM 的话)你多快能用 Go + SQLite 做出一套认证系统、一个表单、只允许已登录用户保存答案、并带上合理的安全默认值?然后请对普通 Go 开发者和普通 Django/Rails 开发者再问一遍这个问题。」nargek 也说:选 Django 的人都知道 Python 天生不会是最高效的,但 Django 久经考验,能很快做出稳定的产品。
- hecreto 安利 .NET:「不想扫兴……但大家真该试试 .NET。我不知道我们是不是只是看着这些东西说『这是新东西吗?我们不是做了几十年了吗』的老古董,但 .NET 开箱即用加上几个数据库、日志的包,舒服到你根本不用想它。」Izkata 回敬:Django 现在同样够格算「几十年」了,这篇文章里的大部分东西在它刚出来时就适用,基本设计已经稳定了很久。rileymat2 补刀:对老古董来说 Python 早于 .NET,而他 2008 年就在用 Django,那早于 .NET MVC。
- melodyogonna 给了一个不常见但真实的理由:「Django 很好,但我正在把一个项目从 Django 迁到 Go,因为 Django 太大了。我很想留下,但光是消化文档就是苦差事——PDF 有 3000 页,而我喜欢所有部件都能装进脑子里的工具。」
其他
- sakjur 对作者的元评论表示赞赏:「我非常欣赏 Julia 这一点。她的文字和 zine 探索软件的方式是在鼓励好奇心,而不是推销某一个唯一的观点。」
- trojans1290 问了个所有人都在想的问题:「2026 年了,Django 对新后端项目还是稳妥选择吗?我一直很喜欢它的 ORM、admin 之类。」
- coolThingsFirst 贡献了跑题金句:「Web 开发无聊得让人麻木。学计算机科学去做 Web 开发,就像学物理是为了打开烤箱。」
- rubyfruit 只留了四个字:「Holy 2003 that website」(这网站真有 2003 年味)。