
从 ORM 实践中得出与其依赖 ORM不如直接学 SQL我得出结论对我而言对象关系映射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。多数时候ORM 生成的 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 让一切变得简单”的陷阱。ORM 可以作为一种可接受的数据定义表示方式但不适合编写查询也不适合存储对象状态。如果你使用的是关系型数据库管理系统RDBMS那就咬紧牙关学习 SQL 吧。2014 年 8 月 3 日[commentwozniak.ca](mailto:commentwozniak.ca)生成于 2022 年 1 月 2 日