跳到主要内容

你可能并不需要ORM(Object–relational mapping)

用语
解释
OOObject-Oriented,泛指面向对面概念模型。
OOPObject-Oriented Programming,基于OO的编程范式。
EREntity–relationship。一种遵循关系建模范式的数据模型。
阻抗失谐impedance mismatch。形容概念模型之间存在映射成本。该词汇常用于 OO、ER 等上下文。
抽象(计算机科学)对具体细节进行概括的过程。本质上是一种通过屏蔽细节信息来简化理解或应该的解决方案。
抽象泄漏违反了抽象本身的原则,令到相关人员需要了解抽象背后的细节信息。对于技术工具而言,抽象泄漏会让开发者产生额外的理解和学习成本。
数据一致性问题因数据存在多个版本而引发的逻辑错误问题。
POPersistence Object。指代那些用于映射成 ER(数据模型)的内存模型。
DTOData transfer object。一种应用于上下文边界之间的数据载体。同时也是一种解耦模式。

ORM 的诞生主要是为了解决OO和ER之间存在的阻抗失谐问题。 帮助开发者处理 OO(内存模型)和ER(数据模型)的映射关系,简化持久层编码工作。然而时至今日,其实 ORM 在业界仍然是备受争议。 有人认为,它可以让简单问题变得更简单,但复杂问题也会变得更复杂1。 亦有人说,它是一种抽象失败的案例2,因为其实现基本都存在抽象泄漏问题;它无法做到让开发者摆脱 SQL,甚至还需要开发者了解 ORM 本身的实现细节(譬如:N+1问题3)。 结合相关实现(框架)的学习曲线(通常较高)复杂性等考量因素,使用 ORM 通常是付出大过收益。

衡量一个决策是否优秀的第一原则就是看它与需求的契合程度。所以当你在“是否应该采用 ORM”这种问题上犹豫不决时,其实最应该做的事是重新理解 ORM 的特性。

前文提到,ORM 主要用于解决OO和ER模型之间的阻抗失谐问题。 个人认为,内存模型越是符合 OO 范式设计,那么 ORM 就会越适用。 另外,ORM 可以让开发者可以尽可能少地关注 SQL。而这可以显著地提高开发效率。特别是配合spring data jpa使用时,效果尤为显著。 但问题在于透过关系映射自动生成的 SQL 通常难以满足性能要求。而且其查询能力会受到框架限制4。但这些问题其实可以通过 Native SQLQuerydslProjectionsQueryRewriter 等方案解决。 通常 ORM 还拥有较好的移植性。譬如有规范(如:JPA)支持切换框架实现,并且能够根据具体的数据库类型来生成方言 SQL。但经常维护过度设计系统的开发者可能会认为这是一种伪需求。因为他们很少遇到需要变更数据库类型的情况。然而,受到良好设计思想(如:KISS原则)约束的系统可能会因为用户或数据负载扩张而需要变更数据库类型;企业也可能会出于资金战略原因采取同样的措施。但不论何种原因,变更数据库类型都是一项棘手的任务。此时可将 ORM 视为一种能一定程度降低数据库类型变更成本的方案。 此外,ORM 框架通常会内置缓存层实现来避免不必要的 SQL 查询。但这种功能一般只适用于单体应用。因为横向伸缩节点时,可能会因为无法感知其他节点的更新操作而引发数据一致性问题5

相较于 ORM 而言,采用非 ORM 框架会使得开发者无法回避 SQL 知识。但这通常不会成为问题;因为对于后端开发而言,这本身就是一项必备的技能。 另外,非 ORM 框架通常抽象程度较低。这意味着开发者需要更多的工作量,但好处是能够拥有更高的灵活性来满足性能复杂查询需求。而且根据具体实现的差异,其实也会提供一定程度的移植性阻抗失谐解决方案。譬如 JOOQ 就提供移植性。 值得注意,中国境内目前最流行的(Java)非 ORM 框架是 MyBatis6(数据源自:google trends)。其优势在于能够集中式管理SQL。而在灵活性方面,MyBatis 提供了动态标签。但相较于 JOOQ,它会将部份代码逻辑下推到 XML 中。个人并不喜欢这种设计。

整体而言,ORM 更适合注重开发效率开发周期较短的项目。 但从长远来看,非 ORM 通常是更好的选择。特别是自研产品,灵活性要比开发效率更有价值。因为可扩展的前提是架构本身需要是灵活的。

拓展:善用投射(Projections)

投射的概念并非特定于 ORM 上下文。实际上你可以在任何数据交互边界中应用它。 这样做的好处是,可以有效地实践接口隔离原则(提供者角度)7迪米特法则(消费者角度)

另外,用于投射的数据类型本质上是一种 DTO,而非 PO。 但为其命名时并不建议采用“Dto”作为后缀。这样做貌似在强调它与 PO 之间的区别(。但事实上它又确实从持久层中来)。所以个人认为以“Projection”作为后缀可能会更为妥当一些。但更好的做法是将相关数据类型组织到一个源码文件中。例如 UserProjection.Address、UserProjection.Profile。这样做可以有效地避免命名所带来的困扰。

参考


  1. ORM 的使用复杂度与内存模型本身的复杂度成正相关关系。 ↩︎

  2. ORM 属于抽象失败这一观点,个人理解针对的是 ORM 实现(即框架)。因为 ORM 本身只是一种概念。 ↩︎

  3. N+1 是 ORM 实现中的一个备受争议的问题。它描述这么一种情况,当查询一个存在嵌套关系的内存模型(PO)时,就会产生额外针对关联目标的 SQL 查询。至于需要执行多少额外的 SQL 查询,则取决于 N 的大小,而 N 代表关联目标的数量。N+1 问题的核心在于批量执行 SQL 查询会降低系统性能(,因为涉及网络和磁盘I/O)。该问题常见的解决方案有(1)应用懒加载策略。只在访问关联目标时才执行额外查询。(2)使用JPA JOIN FETCH查询。预加载所有关联目标。 ↩︎

  4. ORM 框架的查询能力会受到自身设计影响。譬如近年来一些关系型数据开始支持 JSON 数据类型,但 ORM 框架则未必支持(至少需要些时间)。 ↩︎

  5. 微服务架构风格下,服务实例应该是无状态的(具体而言,不应该有更新相关的状态)。因为状态越少,伸缩性越好。 ↩︎

  6. 个人认为 MyBatis 之所以在中国流行,主要是因为一些所谓的“大厂”。这里其实反应着一个行业事实(至少中国是这样)。很多中小型企业和团队其实都缺乏良好的软件工程实践知识和经验,所以他们只能根据“大厂”的指引来作出决策。其底层逻辑是希望通过复制大厂的决策来取得同样的成功。 ↩︎

  7. ISP 中的接口并非特指编程语言中的抽象特性(如:interface),而是指代服务提供者对外暴露的 API。它是一种抽象概念。例如对象的 Getter 对于其客户端而言就属于接口。 ↩︎

Dik Tam
作者
Dik Tam
只是喜欢写代码。