ORM教会我的:直接学SQL (2014)
摘要
作者认为ORM弊大于利,开发者应直接学习SQL,以避免属性蔓延和低效查询等问题。
暂无内容
查看缓存全文
缓存时间: 2026/07/04 15:40
# ORMs教会我的:只需学习SQL
来源:https://wozniak.ca/blog/2014/08/03/1/index.html
我最终得出的结论是,对我来说,ORM的弊大于利。简而言之,它们可以很好地辅助在程序中与SQL打交道,但不应取代SQL。
一些背景:在过去30个月里,我一直在处理需要与Postgres以及一定程度上与SQLite交互的代码。其中大部分工作使用了SQLAlchemy (http://sqlalchemy.org/)(我挺喜欢)和Hibernate (http://hibernate.org/)(我不喜欢)。我既处理过现有代码和数据模型,也设计过自己的模型。大多数数据是基于事件的存储(“时间线”),并且非常注重创建报告。
关于对象/关系阻抗不匹配,人们已经写了很多。只有亲身经历才能真正体会。Neward在他那篇著名的文章 (http://blogs.tedneward.com/post/the-vietnam-of-computer-science/)中,列举了许多令人信服的原因,说明ORM为何会变成泥潭。根据我的经验,我不得不直接面对相当多的这类问题:实体标识问题、双重模式问题、数据检索机制问题以及部分对象问题。我想简要谈谈我对这些问题的体验,并补充一个我自己的看法。
## 部分对象、属性蔓延和外键
也许ORM给我带来的最具侵蚀性的问题是“属性蔓延”或“宽表”,也就是不断积累属性的表。尽管我很想避免这种情况,但有时它变得不可避免(尽管像Postgres的hstore (http://www.postgresql.org/docs/9.3/interactive/hstore.html)这样的东西可以提供帮助)。例如,客户可能会提供大量数据,他们希望根据各种业务逻辑将这些数据附加到报告中。此外,你对这些数据了解不多;你只是在搬运它们。
这在数据库中本身并不是什么可怕的事情。但在ORM中,这就成了一个真正的痛点。具体来说,问题开始出现在任何直接使用实体来创建查询的查询中。在项目初期,你可能有一个像这样的Hibernate查询:
``
query(Foo.class).add(Restriction.eq("x", value))
``
当Foo只有五个属性时,这可能没问题,但当它有一百个属性时,这就变成了数据消防水管。这相当于使用了`SELECT *`,通常表达的意思超出了实际意图。然而,ORM鼓励这种用法,并且常常使得编写精确的投影变得和SQL一样繁琐。(我通过添加适当的投影优化了此类查询,将运行时间从几分钟减少到几秒;所有时间都花在了将数据库行转换为Java对象上。)
这又导致了另一个糟糕的体验:对外键的恶性使用。在我使用过的ORM中,类之间的链接在数据模型中表示为外键,如果配置不当,在检索对象时会导致大量的连接操作。(在我工作中的一个最近统计中,某个表有超过600个属性和14个连接才能访问单个对象,使用的还是首选的查询方法。)
属性蔓延和过度使用外键告诉我的是,为了有效使用ORM,你仍然需要懂SQL。我对ORM的异议在于,如果你需要懂SQL,那就直接用SQL,因为它避免了需要了解非SQL如何被翻译成SQL的问题。
## 数据检索
当你尝试实际使用ORM编写查询时,懂得如何编写SQL就变得更加重要。当效率成为关注点时,这一点尤其重要。
据我观察,除非你有一个非常简单的数据模型(也就是说,你从不做连接),否则你会费尽心思去弄清楚如何让ORM生成运行高效的SQL。大多数时候,它比实际SQL更晦涩难懂。
如果你选择保持查询简单,那么你最终会在代码中做很多工作,而这些工作本可以在数据库中更快地完成。窗口函数 (https://en.wikipedia.org/wiki/Window_function_%2528SQL%2529#Window_function)是相对高级的SQL,用ORM编写起来非常痛苦。不将它们写入查询,很可能意味着你会将大量额外数据从数据库传输到应用程序中。
在这些情况下,我选择使用模板系统编写查询,并用ORM来描述表。我既得到了应用程序级别对表的描述,又直接使用了SQL。这比我到目前为止使用过的任何其他方法都要省事得多。
## 双重模式的危险
这似乎是那种无法避免的冗余之一。如果你试图摆脱它,只会造成更多问题或增加过多的复杂性。
问题是,你最终会在两个地方定义数据结构:数据库和应用程序。如果你完全在应用程序中维护定义,你最终将不得不使用ORM代码来编写SQL数据定义语言(DDL),这和用ORM编写高级查询一样复杂。如果你将定义保留在数据库中,你很可能会为了便利和避免过多的“字符串输入”而在应用程序中为其创建一个表示。
我更倾向于将数据定义保留在数据库中,并将其读入应用程序。这并不能解决问题,但会使其更易于管理。我发现使用反射技术获取数据定义并不值得,我不得不屈服于在两个地方管理数据定义的冗余性。
但该死的迁移问题才是真正的痛击:在应用程序中修改模型没什么大不了的,但在数据库中却是一个真正的痛苦。毕竟,数据库是持久化的,而应用程序数据不是。ORM在这方面只会碍事,因为它们根本不帮助管理数据迁移。我遵循的原则是,数据库中的数据定义不是你应该在应用程序中操作的东西。相反,操作查询的结果。也就是说,查询是你的数据库API。因此,不是考虑对象,而是考虑带有返回类型的函数。
因此,人们不得不问,除了在构建查询时提供便利之外,是否应该使用ORM做其他事情?
## 标识
处理实体标识是你在使用ORM时必须时刻牢记的事情之一,迫使你为两个系统编写代码,而表达能力只相当于一个系统。
当你有外键时,你会用标识符来引用相关的标识。在应用程序中,“标识符”有不同的含义,但通常是指内存位置(指针)。在数据库中,它是对象本身的状态。这两者并不真正兼容,因为你实际上只能在数据库中使用数据库标识符(你正在处理的数据的最终目的地)。
这导致的结果是,你必须操作ORM来获取数据库标识符,方法是通过手动刷新缓存或执行部分提交来获得实际的数据库标识符。
我甚至不能称之为泄漏抽象,因为“泄漏”这个词暗示相对于源来说,只有少量内容逃逸。
## 事务
Neward 暗示了一点,就是开发者需要处理事务。事务是动态作用域的,这是一个强大但在编程语言中大多被忽视的概念,因为过度使用会导致混乱。这导致大量带有异常处理器的样板代码,并且需要仔细考虑事务边界应该在哪里。它还迫使你将会话对象传递给任何可能要与数据库通信的函数/方法。
事务的概念很难很好地翻译到应用程序中,因为它们依赖于基于时间的上下文。如前所述,动态作用域是在程序中使用它的一种方式,但它与主流范式——词法作用域相冲突。因此,在编写与数据库打交道的代码时,你必须非常小心地了解事务的“何时”,这可能会使模块化变得棘手(“这是一个有用的函数,但只会在特定上下文中工作”)。
## 我未来的方向?
此时,我开始质疑彻底拒绝存储过程 (http://c2.com/cgi/wiki?StoredProcedures) 是否明智。这听起来有些异端 (http://c2.com/cgi/wiki?StoredProceduresAreEvil),但它可能适用于我的用例。(而且,随着“DevOps”的出现,开发人员和数据库管理员之间的鸿沟基本上已不存在。)
我发现自己在思考,数据库只是另一种具有API的数据类型:查询。查询返回某种类型的值,在程序中表示为某个对象。通过不再将应用程序中的对象视为要存储在数据库中的东西(ORM存在的理由),而是将数据库视为一个(庞大而复杂的)数据类型,我发现从应用程序中使用数据库要简单得多。并且好奇为什么我早点没发现这一点。
(需要明确的是,我并不是声称所有应用程序都应该这样处理数据库。我只是说,这适合我基于所处理数据的用例。)
无论我最终是否发现存储过程并非那么邪恶,还是继续使用模板化SQL,我确实知道一件事:我不会掉入“ORM让一切变得简单”的陷阱。它们是一种可接受的数据定义表示方式,但编写查询时却很糟糕,并且存储对象状态时也很差。如果你在使用关系型数据库,就咬咬牙,学好SQL吧。
相似文章
Genie Ontology 如何真正提高 text-to-SQL 准确率——机制而非宣传
本文解释了 Genie Ontology 方法如何通过关注底层机制而非营销宣传来提高 text-to-SQL 准确率。
@omarsar0:推荐阅读。(收藏)注意价格以及模型组合能为你解锁什么。你……
一条讨论Cursor实验的推文:一组AI代理根据手册用Rust重写了SQLite,实现了100%的测试通过率,但成本因模型组合不同而有显著差异。要点包括:使用前沿模型进行分解,使用更便宜的工人进行实现。
@addyosmani: https://x.com/addyosmani/status/2056078124346228860
Addy Osmani 提醒:过度依赖 AI 编写代码可能会阻碍学习,并削弱对软件开发的思维模型。
使用枯燥语言配合LLM
一篇观点文章指出,LLM在枯燥且一致的语言与生态系统(如Ruby on Rails)中表现更佳,因为训练语料库的方差较低,从而产生更可靠的智能体输出,而碎片化的生态系统(如JavaScript)则导致效果不佳。
@ArizePhoenix: 机器学习中最古老的教训之一,对于使用 LLM 应用仍然非常有用:不要用相同的数据进行评估……
本文讨论了使用 Arize Phoenix 开发 LLM 应用的最佳实践,特别强调了使用训练集/验证集/测试集拆分来进行诚实评估和追踪回归的重要性。