首页 / 文章 / 是时候学习 SQL 了:ORM 教会我的那些事
← 返回
AI技术

是时候学习 SQL 了:ORM 教会我的那些事

✍️ zhirenhun 📅 2026/7/5 👁 66 阅读 ⏱ 8 分钟
是时候学习 SQL 了:ORM 教会我的那些事

是时候学习 SQL 了:ORM 教会我的那些事

我得出了一个结论:对我来说,ORM 的弊大于利。简而言之,它们可以用来很好地辅助在程序中使用 SQL,但不应取代 SQL。

先交代一些背景:过去 30 个月里,我一直在与需要对接 Postgres(以及一定程度上的 SQLite)的代码打交道。其中大部分工作使用了 SQLAlchemy(我非常喜欢)和 Hibernate(我不太喜欢)。我既维护过现有的代码和数据模型,也设计过自己的模型。这些数据大多是基于事件的存储("时间线"),并且非常注重报表生成。

关于"对象/关系阻抗不匹配"(Object/Relational Impedance Mismatch),已有大量文章讨论过。只有亲身经历过,才能真正体会其棘手之处。Neward 在他那篇著名的文章中列举了许多令人信服的理由,说明 ORM 为什么会变成泥潭。根据我的经验,我直接处理过其中不少问题:实体标识问题、双模式问题、数据检索机制问题,以及部分对象问题。我想简要谈谈我在这些问题上的经历,并补充一个我自己遇到的。

部分对象、属性膨胀与外键

ORM 带给我的最具破坏性的问题,或许是"属性膨胀"或"宽表"——也就是说,表不断地累积属性。虽然我很想避免这种情况,但有时它又不可避免(尽管 Postgres 的 hstore 等功能能有所帮助)。

在数据库中,这本身并不是什么可怕的事情。但放到 ORM 中,它就成了真正的痛点。具体来说,当你使用实体对象直接构建查询时,问题就会暴露出来。比如你可能有这样一个 Hibernate 查询:

query(Foo.class).add(Restriction.eq("x", value))

Foo 只有五个属性时,这没什么问题;但当它有一百个属性时,就成了一根数据消防水管。这等价于使用 SELECT *,而后者通常表达的远比你想要的要多。然而,ORM 鼓励这种用法,并且往往让编写精确的投影查询变得和在 SQL 中一样繁琐。

这又引出了另一个糟糕的体验:外键的滥用。在我用过的 ORM 中,类之间的链接在数据模型中通过外键来表示,如果没有仔细配置,在检索对象时就会导致大量 join 操作。

属性膨胀和外键的过度使用让我意识到:要想有效使用 ORM,你仍然需要懂 SQL。我对 ORM 的质疑在于,如果你已经需要懂 SQL,那直接用 SQL 就好了,因为这样你就不需要再去理解非 SQL 是如何被翻译成 SQL 的了。

数据检索

当你真正尝试用 ORM 编写查询时,掌握 SQL 就变得更加重要了。尤其是在需要考虑效率的时候。

根据我的观察,除非你的数据模型非常简单(即从不做 join),否则你会绞尽脑汁去想办法让 ORM 生成高效运行的 SQL。大多数时候,ORM 生成的查询比手写 SQL 更加晦涩难懂。

如果你选择让查询保持简单,那你就不得不在代码中做大量本可以在数据库中更高效完成的工作。窗口函数(Window functions)是相对高级的 SQL 功能,用 ORM 来实现相当痛苦。如果不在查询中写入这些功能,通常意味着你要从数据库往应用程序传输大量额外的数据。

双模式的风险

这似乎是一种无法避免的冗余。如果你想摆脱它,只会制造更多问题或增加过多的复杂性。

问题在于你最终会在两个地方维护数据定义:数据库和应用程序。如果你完全把定义放在应用程序中,那就不得不在 ORM 代码中编写 SQL 数据定义语言(DDL)。如果把它放在数据库中,为了方便,你又可能希望在应用程序中有一个表示层。

那该死的迁移问题更是雪上加霜:在应用程序中修改模型不算什么大事,但在数据库中却非常痛苦。毕竟,数据库是持久化的,而应用程序数据不是。ORM 在这里不仅帮不上忙,反而碍手碍脚,因为它们根本无法帮助管理数据迁移。

标识(Identity)

处理实体标识是在使用 ORM 时必须时刻牢记的事情之一,它迫使你同时为两套系统编写代码,却只能使用一套系统的表达能力。

当你使用外键时,你需要通过标识符来引用关联的实体。在应用程序中,"标识符"有多种含义,但通常是指内存地址(指针)。而在数据库中,它指的是对象的状态本身。

事务

Neward 在文章中暗示了一点:开发者需要处理事务。事务是动态作用域的(dynamically scoped),这是一个强大但基本上被编程语言所忽视的概念,因为过度使用会造成混乱。这导致了大量带有异常处理器的样板代码,并且需要仔细考虑事务边界应该设置在何处。

由于事务依赖于基于时间的上下文,这个概念很难在应用程序中很好地转化。动态作用域是在程序中使用它的一种方式,但它与当今主流的词法作用域(lexical scoping)范式相冲突。

我未来的方向

此时此刻,我开始质疑完全拒绝存储过程(stored procedures)是否明智。这听起来有点异端,但它可能适合我的使用场景。

我发现自己在把数据库看作另一种数据类型,它有一个 API:查询。查询返回某个类型的值,在程序中表示为某个对象。如果不再把应用程序中的对象视为需要存储到数据库中的东西(这正是 ORM 存在的理由),而是把数据库看作一个(庞大而复杂的)数据类型,我发现从应用程序中操作数据库变得简单多了。

无论我最终发现存储过程其实没那么可怕,还是继续使用模板化 SQL,有一件事我很确定:我不会再掉进"ORM 让一切变简单"的陷阱里。ORM 在表示数据定义方面尚可接受,但用来编写查询就很糟糕,用来存储对象状态就更差了。如果你正在使用关系数据库,咬咬牙,去学 SQL 吧。


原文出处:wozniak.ca

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

SQL ORM 数据库 Postgres 编程实践
← 上一篇
FreeBSD 吃掉了我的内存!
下一篇 →
知识不应被关在笼子里:当 RAG 遇上 Markdown

📌 相关推荐

结构化输出 vs 工具使用 vs 预填充:2026年从Claude获取JSON
2026/7/11
在AWS Bedrock上构建生产级AI代理——值得关注的架构与代码决策
2026/7/11
如何为你的AI代理构建评估框架(确保其在生产环境中不崩溃)
2026/7/11
← 返回文章列表