ORM:甜蜜的陷阱,还是生产力杀手?
声明:本文不推荐任何 ORM,也不反对任何 ORM。本文只是想把那些你在入门教程里看不到的真相,撕开给你看。
一、我们先来玩个游戏
假设你现在要查一个订单详情,需要:
- 订单基本信息(订单号、金额、状态)
- 下单用户信息(名字、手机号、会员等级)
- 订单关联的商品列表(商品名称、数量、单价)
- 每个商品的库存情况
- 优惠券信息
- 物流信息
请问:用 ORM 怎么写?
order = Order.objects.select_related('user').prefetch_related(
'items__product__inventory',
'coupon',
'logistics'
).get(id=order_id)
优雅,太优雅了。三行代码搞定一切。
但你知道这里发生了什么吗?
- 1 次主查询
- 1 次 user 表 join
- N 次 items 查询(prefetch)
- N 次 product 查询
- N 次 inventory 查询
- 1 次 coupon 查询
- 1 次 logistics 查询
如果一个订单有 5 个商品,恭喜你,一次页面请求触发 13 次数据库查询。
这就是我要说的第一件事:ORM 最大的谎言,就是让你以为查询是免费的。
二、你看文档里不是这么写的
你可能用过 Django ORM 的 select_related 和 prefetch_related,觉得可以解决 N+1 问题。
实际开发中,至少 80% 的 ORM 性能问题,都来自于开发者以为自己在写高效查询,其实只是在写「看起来像查询」的代码。
三、类型系统的消失
ORM 第二个坑:类型安全感。
Python 是动态类型,Java 是静态类型。但无论哪种语言,一旦进了 ORM 的世界,你的数据库类型就变成了薛定谔的猫。
原始 SQL 至少明确:INT 就是 INT,VARCHAR 就是 VARCHAR。ORM 给你的是一种虚假的抽象。
很多人说 ORM 可以换数据库。兄弟,你换过吗?
真正换过数据库的人都知道,90% 的代码得重写,不是因为 SQL 语法,而是因为 ORM 的那些「高级特性」在另一个数据库里根本不支持。
四、事务的温柔陷阱
with transaction.atomic():
order = Order.objects.create(...)
Inventory.objects.filter(product_id=id).update(stock=F('stock') - 1)
看起来原子性有了。但你知道 F() 表达式在并发情况下可能出问题吗?
高并发秒杀场景下,F() 更新库存不是原子的。
这就是丢失更新问题。
正确做法是用原生 SQL 加上条件判断:
cursor.execute(
"UPDATE inventory SET stock = stock - %s WHERE id = %s AND stock >= %s",
[quantity, product_id, quantity]
)
ORM 掩盖了并发问题的复杂性,让你在开发环境里岁月静好,然后在生产环境里定时爆炸。
五、我什么时候会用 ORM
说了这么多 ORM 的坏话,是不是该反转了?其实不是。我用 ORM,但有条件:
1. 简单的 CRUD 系统
后台管理系统、内部工具、数据展示页面——这种场景 ORM 就是爽。
2. 团队里都是 junior
ORM 至少能让你不写 SQL 注入漏洞。
3. 性能不敏感的 MVP
快速原型阶段,ORM 能让你少写 50% 代码。先跑起来再说。
但如果是:
- 高并发交易系统
- 复杂查询报表
- 需要深度优化的核心业务
兄dei,回到 SQL 吧。
六、正确的使用姿势
如果你决定用 ORM,请记住这几点:
1. 永远知道最终生成的 SQL 是什么
Django: print(query.query)
SQLAlchemy: print(str(statement))
2. 复杂查询该写 raw 就写 raw
别为了「优雅」硬凑 ORM 方法,那点语法糖不够赔的。
3. 做好监控和慢查询日志
不是你的代码慢,是你的代码不知道自己在写什么。
4. 单元测试里加上 SQL 计数
with self.assertNumQueries(2): # 必须是 2 条,多一条都不行
...
写在最后
ORM 就像方便面包装上的牛肉——图片仅供参考。
它确实简化了入门门槛,但同时也隐藏了太多复杂度。当你觉得 ORM 不够用的时候,通常不是 ORM 的问题,而是你面对的问题本身就很复杂。
解决复杂问题的第一步,是承认它复杂。
不要因为写起来爽,就以为它真的是简单的。
共勉。
本文可能引发 ORM 信仰者不适,但这就是真实的工程选择。peace。