从ORM学到的教训:不如直接学SQL
速览
作者通过多年使用ORM的经历,发现ORM虽然简化了代码,但增加了复杂性、隐藏了SQL细节,导致性能问题和调试困难。他总结出学习SQL比依赖ORM更有效、更可控。这篇文章鼓励开发者回归基础,掌握SQL本身,而不是过度依赖ORM工具。
AI 深度解读
背景
2014年前后,对象关系映射(ORM)框架如 Hibernate 和 SQLAlchemy 正处于广泛使用的高峰期,许多开发者试图用 ORM 完全替代 SQL,以追求开发效率。然而,作者在 30 个月的 Postgres 和 SQLite 实战中(主要使用 SQLAlchemy 和 Hibernate),发现 ORM 带来的麻烦远多于便利,最终得出「只需学习 SQL」的结论。这一反思在 Hacker News 上引发了大量讨论,至今仍有启发意义。
核心内容
作者认为,ORM 可以作为 SQL 的有益补充,但绝不应取代 SQL。他总结了几个亲身经历的痛点:
部分对象、属性蔓延与外键的滥用
最隐蔽的问题是「属性蔓延」——表不断添加新字段。虽然数据库本身可以承受,但 ORM 会放大问题。例如,使用 Hibernate 的 query(Foo.class).add(Restriction.eq("x", value)) 在 Foo 只有 5 个属性时尚可,但一旦增长到上百个属性,这种方式等同于 SELECT *,导致大量不必要的数据传输。作者曾通过添加精确投影优化查询,将运行时间从几分钟降到几秒——所有时间都浪费在将数据库行转换为 Java 对象上。
外键的滥用同样严重。ORM 中的类关联通常映射为数据库外键,若配置不当,检索单个对象会触发大量 JOIN。作者工作中某张表最终有超过 600 个属性和 14 个 JOIN。这让他意识到:要有效使用 ORM,你仍然需要懂 SQL;既然必须懂 SQL,直接用 SQL 反而省去了解 ORM 翻译过程的心智成本。
数据检索效率
当需要实际编写查询时,懂 SQL 变得更为重要。要么花大力气让 ORM 生成高效的 SQL(通常比纯 SQL 更晦涩),要么保持查询简单而在应用层做大量本可在数据库中更快完成的工作。例如窗口函数这类相对高级的 SQL,用 ORM 编写极为痛苦,放弃使用则意味着在应用和数据库之间传输大量额外数据。作者的做法是用模板系统写查询,同时用 ORM 描述表结构,既获得应用层表描述便利,又保留直接 SQL 的威力。
双模式(Dual Schema)危机
数据定义同时存在于数据库和应用中,这是无可避免的冗余。若完全将定义放在应用,则必须用 ORM 代码编写 DDL,与在 ORM 中编写高级查询同样复杂;若完全放在数据库,又希望应用中有便利的表示以防止过多「字符串打字」。作者倾向于将数据定义保留在数据库,再读入应用。但这并未解决问题,只是使其更可控。反射获取数据定义不值得,他只能接受在两个地方管理冗余。最令人头疼的是数据迁移:修改模型在应用中轻而易举,在数据库中却是大麻烦——数据库是持久化的,应用数据不是。ORM 对此毫无帮助。作者认为,不应在应用中操作数据库的数据定义,而应操作查询结果,将查询视为数据库的 API。
身份标识(Identities)
处理实体身份是 ORM 使用中必须时刻注意的问题。外键在数据库中用标识符引用相关实体,而在应用中「标识符」往往是内存地址(指针)。两者无法直接兼容,因为只有数据库标识符在数据库中才有意义。这导致需要手动刷新 ORM 缓存或部分提交来获取真正的数据库标识符。作者甚至认为这已不算是「漏掉的抽象」(leaky abstraction),因为「漏」意味着少量泄露,而这里几乎整个抽象都是漏洞。
事务
事务具有动态作用域,是程序中强大但常被忽视的概念(因为滥用会造成混乱)。这导致大量带有异常处理的样板代码,且需要仔细考虑事务边界。你不得不在所有可能涉及数据库通信的函数/方法中传递 session 对象。事务概念基于时间上下文,难以很好地映射到应用层编程模型(后者依赖词法作用域)。编写数据库相关代码时必须时刻了解事务的「何时」,使模块化变得棘手——一个看似有用的函数只能在特定上下文中工作。
作者最后开始质疑完全拒绝存储过程的传统智慧,并暗示随着 devops 的兴起,开发与运维的界限正在模糊。文章因截断而未完整收尾,但核心观点已鲜明呈现。
关键要点
- ORM 可作为 SQL 的补充,但不应替代 SQL;使用 ORM 必须懂 SQL,既然如此,不如直接用 SQL。
- 属性蔓延(宽表)在数据库层尚可接受,但在 ORM 中极易导致
SELECT *式的低效查询,必须显式指定投影。 - 外键滥用会导致检索一个对象触发大量 JOIN(作者见过 14 个 JOIN),配置时必须谨慎。
- ORM 生成高效 SQL 往往比直接写 SQL 更困难,尤其在涉及窗口函数等高级功能时。
- 双模式(数据库定义 + 应用定义)是难以消除的冗余;数据迁移问题 ORM 完全无法解决。
- 实体身份标识在数据库(值)和应用(指针)之间不匹配,需要手动干预(如 flush 缓存)。
- 事务的动态作用域与编程语言的主流词法作用域冲突,导致大量样板代码和模块化难题。
- 作者倾向于将查询视为数据库的 API,而非操作对象。
意义与影响
这篇文章虽然发表于 2014 年,但其对 ORM 的批评至今仍具有现实意义。许多现代 ORM(如 Prisma、Entity Framework Core)虽然改进了延迟加载、投影自动优化等机制,但底层对象-关系阻抗不匹配(Object/Relational Impedance Mismatch)依然存在。作者的核心观点——「ORM 不是 SQL 的替代品,而是辅助工具;学习 SQL 才是根本」——已成为许多后端开发者的共识。文章中提到的属性蔓延、双模式冗余、事务边界等问题,在当前微服务和复杂查询环境下依然频繁出现。它促使开发者重新审视 ORM 的合理使用边界:对于简单 CRUD 和原型快速开发,ORM 能提升效率;但对于复杂报表、数据密集型分析或需要精细控制性能的场景,直接使用 SQL 或查询构建器往往更优。此外,作者对存储过程的态度松动,也预示了后来「在数据库中执行逻辑」的回归(如 PostgreSQL 的存储过程与函数),以及更现代的「数据库即 API」理念。总之,这篇文章是一剂清醒剂:工具再强大,也不应取代对底层原理的掌握。
