原生SQL、Query Builder和ORM的区别

引言

使用数据库来管理应用程序的数据,是数据持久化最常见的选择之一。数据库能够快速存储和检索信息,提供数据完整性保障,并在单个应用实例的生命周期之外实现持久化。市面上有无数种数据库可供选择,以满足项目需求和个人偏好。

然而,直接从应用程序操作数据库并不总是容易的。数据结构表示方式的差异常常带来挑战,表达不同实体之间关系的细微之处也很困难。为了解决这些问题,人们开发了许多工具,作为核心应用与数据层之间的接口。

在本文中,我们将探讨三种常见方法之间的差异:原生 SQL、Query Builder和 ORM。我们将比较每种方法的优缺点,并在最后提供一个常用术语表,帮助你熟悉一些关键概念。

以下是每种方法的优缺点概览:

方法 面向数据库/编程 手动管理程度 抽象层级 复杂度
原生 SQL 面向数据库
Query Builder 混合
ORM 面向编程

使用原生 SQL 或其他数据库原生查询语言管理数据

有些应用程序通过使用数据库引擎支持的原生语言编写并执行查询,直接与数据库交互。通常,只需一个数据库驱动程序即可连接、认证并与数据库实例通信。

开发者可以通过连接发送用数据库原生语言编写的查询。作为回应,数据库也会以其原生格式之一返回查询结果。对许多关系型数据库而言,首选的查询语言是 SQL。

大多数关系型数据库以及一些非关系型数据库都支持结构化查询语言(SQL),用于构建和执行强大的查询。自20世纪70年代以来,SQL一直被用于数据管理,因此它得到了良好的支持,并在一定程度上实现了标准化。

原生SQL的优点

使用SQL或其他数据库原生查询语言管理数据有一些明显的优点。

一个显著的优点是,开发人员编写和管理数据库查询,并显式处理结果。虽然这需要更多的额外工作,但这意味着在数据库存储、表示数据以及检索数据时,没有太多的不确定性。没有抽象层,也意味着更少的额外因素可以导致不确定性。

其中一个例子是性能。虽然复杂的抽象层通过将编程语句转换为SQL查询,但生成的SQL可能效率低下。不必要的子句、过于宽泛的查询以及其他问题都可能导致数据库操作变慢,使得这一切变得更加复杂并难以调试。通过使用原生SQL,你可以运用你的领域知识和常识来避免许多类别的查询问题。

使用原生数据库查询语言的另一个原因是灵活性。任何抽象层都不可能像原生查询语言那样灵活。更高层次的抽象试图弥合两种不同范式之间的差距,这可能会限制它们所能表达的操作类型。然而,当你直接编写原生 SQL 时,可以充分利用数据库引擎的所有特性,表达更复杂的查询。

原生SQL的缺点

尽管原生SQL有一些明显的优点,但它也有一些缺点。

当你在应用程序中使用原生SQL与数据库进行交互时,你必须理解底层数据结构才能编写有效的查询。你需要完全负责应用程序使用的数据类型和结构与数据库中存储的数据类型和结构之间的映射关系。

另一件在使用原生 SQL 时需要牢记的事情是,你必须自行负责输入的安全性。这一点尤其重要,当你存储外部用户提供的数据时,恶意构造的输入可能会诱导数据库泄露你其他私有的数据。

这种类型的攻击被称为SQL注入攻击,当用户的输入数据存在恶意构造的SQL语句时,就会出现这样的问题。高级抽象工具通常会自动对用户输入进行过滤,帮助你避免这类问题。

使用原生SQL几乎意味着你要用常规的字符串来编写查询语句。在某些情况下,如你需要将内容进行转义、或者将字符串拼接起来时,这会非常的麻烦。SQL字符串可能会被多层包裹,极易导致出错。

小结

虽然我们这小节主要是在讨论SQL,但这里的许多信息也适用于其他数据库原生查询语言。

总之,使用原生SQL或任何等效查询语言,能让你最贴近数据库本身用于存储和管理数据的抽象方式,但也迫使你手动承担所有繁重的数据管理工作。

使用Query Builder管理数据

使用一种被称为“Query Builder”的工具或库来与数据库交互,是替代原生 SQL 等数据库查询语言的另一种方案。

什么是Query Builder?

SQL Query Builder在原始数据库原生查询语言之上增加了一层抽象。它们通过将查询模式形式化,并提供方法或函数来添加输入清理和自动转义,使其更容易集成到应用程序中。

使用 SQL Query Builder时,数据库层所支持的结构和操作仍然相对清晰可辨。这使得你既可以用编程方式处理数据,又能保持与数据的距离相对较近。

通常,Query Builder会通过方法或函数来为查询添加条件。开发者可以将这些方法链式调用,从而把这些独立的“子句”组合成完整的数据库查询。

Query Builder的优点

由于Query Builder使用与应用程序其他部分相同的构造(方法或函数),开发者通常认为它们比用字符串编写的原生数据库查询更易于长期维护。区分运算符和数据非常简单,并且可以轻松地将查询拆分为处理查询特定部分的逻辑块。

对某些开发者而言,使用Query Builder的另一个优势在于,它并不会完全屏蔽底层的查询语言。尽管操作是通过方法而非字符串完成,但其透明度较高,熟悉数据库的人仍能轻松理解每个操作的具体作用。而在更高层级的抽象中,这一点往往难以实现。

SQL Query Builder通常还支持多种数据后端,例如抽象化不同关系型数据库之间的一些细微差异。这让你可以在使用不同数据库的项目中使用相同的工具,甚至可能让迁移到新数据库变得稍微容易一些。

Query Builder的缺点

SQL Query Builder也存在一些与原生SQL查询相同的缺点。

一个常见的说法是,SQL Query Builder仍然要求你理解并考虑数据库的结构与能力。对某些开发者来说,这种抽象程度还不够有用。这意味着你不仅要熟悉Query Builder本身的语法和功能,还必须对 SQL 有相当扎实的掌握。

此外,SQL Query Builder仍然需要你自行定义所检索的数据如何与应用程序中的数据相关联。内存中的对象与数据库中的对象之间没有自动同步机制。

尽管Query Builder通常会模仿它们所针对的查询语言,但额外的一层抽象有时意味着某些操作无法通过提供的方法实现。通常,会有一个“原生”模式,可以直接将查询发送到后端,从而绕过Query Builder的常规接口,但这只是回避了问题,而非真正解决它。

小结

总的来说,SQL Query Builder 提供了一层薄薄的抽象,专门针对直接使用数据库原生语言时的主要痛点。它几乎像一个查询模板系统,让开发者在直接操作数据库与增加更高层次抽象之间找到平衡。

使用ORM管理数据

在抽象层级上更进一步的是 ORM。ORM 通常追求更完整的抽象,希望更流畅地与应用程序数据集成。

什么是ORM?

对象关系映射器(ORM)是一类专门负责在关系型数据库的数据表示与面向对象编程(OOP)中内存数据表示之间进行转换的软件。ORM 为数据库中的数据提供了面向对象的接口,试图利用熟悉的编程概念,减少样板代码的编写量,从而加快开发速度。

一般来说,ORM 充当一种抽象层,旨在帮助开发者在不大幅改变面向对象范式的前提下操作数据库。这有助于减轻适应数据库存储格式细节所带来的心智负担。

特别是,面向对象编程中的对象往往会在内部封装大量状态,并通过继承及其他 OOP 概念与其他对象产生复杂关系。要把这些信息可靠地映射到以表为核心的关系型范式,通常并不直观,并且需要对两种系统都有深入理解。ORM 试图通过自动完成部分映射,并为系统内的数据提供富有表现力的接口,来减轻这一负担。

ORM 的挑战是否只存在于面向对象编程与关系型数据库之间?

从定义上讲,ORM 是专门为面向对象的应用语言与关系型数据库之间的交互而设计的。然而,在尝试将编程语言中的数据结构与数据库中的存储的数据结构进行映射和转化时,常常会出现抽象概念不能完全匹配的情况,这就是ORM阻抗匹配问题。

根据编程范式(面向对象、函数式、过程式等)和数据库类型(关系型、文档型、键值型等)的不同,所需的抽象程度也会有所差异。通常情况下,应用内部数据结构的复杂程度决定了与数据存储交互的难易程度。

面向对象编程往往会产生大量具有重要状态和关系的结构,这些都必须被考虑在内。而另一些编程范式则更明确地指出状态存储在哪里以及如何管理。例如,纯函数式语言不允许可变状态,因此状态通常是函数或对象的输入,而这些函数或对象又会输出新的状态。这种数据与操作的清晰分离,以及状态生命周期的明确性,有助于简化与数据的交互。

无论何种方式,通常都可以借助某种软件,在两种不同的数据表示之间进行映射,从而实现与数据库的交互。因此,尽管ORM描述的是这类映射中的一个子集,并且具有特定的挑战,但无论在细节上有何差异,在应用内存与持久化存储之间进行映射往往是必须考虑的问题。

Active record VS ORM

不同的 ORM 采用不同策略来映射应用程序与数据库结构,主要分为两大类:Active Record 模式与 Data Mapper 模式。

Active Record 模式试图将数据库中的数据封装进代码里对象的结构中。对象自带保存、更新或删除数据库记录的方法,对象状态的变化也能轻松地同步回数据库。简单来说,应用中的一个 Active Record 对象就对应数据库里的一条记录。

Active Record 的实现方式允许你通过在代码中创建类与实例来管理数据库。由于它们通常将类的实例直接映射到数据库记录,只要你了解代码中使用了哪些对象,就能很容易地想象出数据库中的内容。

然而,这种模式也存在一些明显的弊端:应用程序往往与数据库高度耦合,这会给数据库迁移(比如更换数据库类型)甚至代码测试带来不小的麻烦。更关键的是,原本该由业务对象自身承担的部分功能,被转移给了数据库,导致代码不得不依赖数据库来补齐这些缺失的能力。此外,由于系统需要将复杂的业务对象无缝映射到底层数据结构,这两者之间的 “隐式自动转换”(类似 ActiveRecord 的 “魔术映射” 特性)也可能引发性能问题。

data mapper模式是另一种常见的 ORM 模式。与 Active Record 模式类似,data mapper也试图充当代码与数据库之间的独立层,在两者之间进行协调。然而,它并不追求将对象与数据库记录无缝集成,而是专注于在保持两者独立存在的前提下进行解耦与转换。这有助于将业务逻辑与涉及映射、表示、序列化等数据库相关细节分离开来。

因此,开发者需要自行负责在对象与数据库表之间建立显式映射,而不是让 ORM 系统自动推断。虽然这能避免紧耦合和“黑箱”操作,但代价是开发者必须投入更多精力来设计合适的映射关系。

ORM的优点

ORM 之所以流行,原因有很多。

它们有助于将底层数据抽象化,使其更易于在应用程序中理解。ORM不再将数据存储是为一个独立的系统,而是帮助你将数据系统作为当前工作的扩展来访问和管理。这能让开发者更快专注于核心业务逻辑,而不被存储后端的细节所困扰。

另一个副作用是,ORM 省去了大量与数据库交互所需的样板代码。ORM 通常自带迁移工具,可根据代码变更自动管理数据库模式。如果 ORM 能帮你调整数据库结构,就不必一开始就把schema设计得完美无缺。应用改动与数据库改动往往同步或紧密关联,让你在修改代码时也能轻松追踪数据库的变更。

ORM的缺点

ORM 并非没有缺点。很多时候,这些缺点恰恰源于让它们变得有用的那些设计决策。

ORM 的一个根本问题在于它试图隐藏数据库后端的细节。这种“障眼法”在简单场景或短期内确实让开发更轻松,但随着复杂度增长,往往会埋下隐患。

这种抽象永远做不到 100% 完备;如果不了解底层的查询语言或数据库结构就贸然使用 ORM,往往会带来有问题的假设,进而让调试和性能调优变得困难甚至不可能。

也许使用 ORM 时最著名的问题就是“对象-关系阻抗失配”(Object-Relational Resistance Mismatch)。这个术语用来描述面向对象编程与关系型数据库所采用的关系范式之间难以顺畅转换的困境。这两类技术所采用的数据模型互不兼容,意味着每增加一分复杂度,就必须引入更多并不完美的抽象层。对象-关系阻抗失配甚至被比作计算机科学领域的“越南战争”:随着项目推进,复杂度只会不断攀升,最终陷入进退两难的境地——既难以成功,也难以掉头。

一般来说,ORM 的速度往往比其他方案慢,尤其在复杂查询时更明显。为了应对各种场景,ORM 通常采用通用模式,即便只是简单的数据库操作,也可能生成冗长复杂的查询。把所有希望都寄托在 ORM 的“万能”上,一旦出错,代价高昂,而且问题往往难以提前发现。

小结

ORM 可以作为一种有用的抽象,让数据库操作变得更加轻松。它们能够帮助你快速设计与迭代,弥合应用逻辑与数据库结构之间的概念差异。然而,这些优势往往也是一把双刃剑:它们可能让你对数据库本身缺乏深入理解,并在调试、范式转换或性能优化时带来挑战。

术语表

在使用与数据库和应用程序交互的技术时,你可能会遇到一些不熟悉的术语。现在简要回顾一些最常见的术语,其中部分前文已提及,部分则尚未涉及。

  • Data mapper(数据映射器): Data mapper是一种设计模式或软件组件,用于将编程数据结构映射到存储在数据库中的数据结构。Data mappers尝试同步两个源之间的更改,同时保持它们的相互独立性。映射器本身负责维护有效的转换,让开发人员能够自由地迭代应用程序数据结构,而无需关心数据库表示方式。
  • Database driver(数据库驱动): Database driver是一种软件组件,旨在封装并实现应用程序与数据库之间的连接。数据库驱动抽象了如何建立和管理连接的底层细节,并提供了一个统一的、程序化的数据库系统接口。通常,数据库驱动是开发人员用于与数据库交互的最低级抽象,更高级别的工具都建立在驱动提供的功能之上。
  • Injection attack(注入攻击): Injection attack是一种攻击,恶意用户尝试通过在面向用户的应用程序字段中使用特制输入来执行不需要的数据库操作。通常,这用于检索不应访问的数据或删除、篡改数据库中的信息。
  • ORM(对象关系映射): ORMs(对象关系映射器)是一种抽象层,用于在关系数据库中使用的数据表示和面向对象编程中使用的内存表示之间进行转换。ORM为数据库中的数据提供面向对象的接口,试图减少代码量并使用熟悉的原型来加速开发。
  • Object-relational impedance mismatch(对象关系不匹配): Object-relational impedance mismatch指的是在面向对象应用程序和关系数据库之间进行转换的困难。由于数据结构差异很大,可能很难如实地、高效地将编程数据结构转换为存储后端使用的格式。
  • Persistence framework(持久化框架): Persistence framework是一种中间件抽象层,用于桥接程序数据和数据库之间的差距。如果持久化框架采用的抽象将对象映射到关系实体,那么它们也可能是ORM。
  • Query builder(查询构建器): Query builder是一种抽象层,通过提供一个受控接口来帮助开发人员访问和控制数据库,该接口增加了可用性、安全性或灵活性功能。通常,查询构建器相对轻量级,专注于简化数据访问和数据表示,而不尝试将数据转换为特定的编程范式。
  • SQL(结构化查询语言): SQL(结构化查询语言)是一种为管理关系数据库管理系统而开发的领域特定语言。它可用于查询、定义和操作数据库中的数据及其组织结构。SQL在关系数据库中无处不在。

总结

在本文中,我们探讨了从应用程序连接数据库的几种不同方案。我们对比了使用数据库原生查询语言(如 SQL)所带来的不同抽象层级与灵活性,也介绍了借助 Query Builder 安全地构造查询,以及通过 ORM 提供更完整抽象层的方式。

每种方案都有其适用场景,某些方案可能比其他方案更适合特定类型的应用。关键是要清楚你的应用需求、团队对数据库的掌握程度,以及所选抽象(或缺乏抽象)所带来的成本。总体而言,深入理解这三种方式,才能最大程度地选出最适合你项目的解决方案。