本文讨论了Node.js中数据库交互的三种主要方法:原生SQL、查询构建器和对象关系映射(ORM)。文章详细解释了每种方法的优点和缺点,以及适用场景,最后提出混合方法,结合多种方法的优点以适应项目需求。
提供了高度的透明度和控制力,特别适合复杂查询和性能优化。但需要编写和维护查询,学习曲线较陡,且可能带来SQL注入的安全风险。
提供了一种更结构化、更安全的方式来构建查询,减少了手动拼接SQL语句的复杂性。简化了基于运行时条件构建动态查询的过程,并降低了SQL注入攻击的风险。但相对于原生SQL,查询构建器可能需要更深入的理解数据库概念。
通过提供高层次的抽象,简化了面向对象编程与关系型数据库之间的差距。减少了样板代码量,加快了开发速度。但可能带来性能开销,失去对数据库操作的细粒度控制,并且有自己的学习曲线。
结合多种方法的优点,以适应手头的项目。例如,使用ORM处理大部分数据访问,同时使用原生SQL来处理性能关键查询或特定数据库功能。
前言
介绍了在 Node.js 中选择使用原始 SQL、查询构建器或对象关系映射器(ORM)的不同方法、优缺点以及适用场景。今日前端早读课文章由 @Damilola Olatunji 分享,@飘飘翻译。
译文从这开始~~
在开发与关系型数据库交互的 Node.js 应用程序时,你可以选择多种工具来管理和执行数据库查询。
其中最常见的三种方式是:原生 SQL、查询构建器(Query Builder)和对象关系映射(ORM)。它们各自有优缺点,也因此常常让人难以抉择。
在本指南中,我们将对这三种方法的优点、取舍和适用场景进行对比,帮助你全面了解它们的特点,并找到最适合你的解决方案。
让我们开始吧!
理解原生 SQL
原生 SQL 是指直接编写和执行 SQL 查询,不依赖任何额外的抽象层。在这种方式下,你需要手动编写 SQL 查询语句,作为普通的字符串直接发送到数据库进行执行:
在你的 Node.js 应用中执行原生 SQL 查询之前,首先需要通过合适的数据库驱动程序,在 Node 应用和你选择的 SQL 数据库之间建立连接。常见的数据库驱动包括:
-
-
-
better-sqlite3(用于 SQLite)
以及其他许多选择!
建立数据库连接后,你可以使用提供的连接对象直接执行 SQL 查询。通常,你需要将查询作为字符串构造,并使用占位符来插入动态值,以防止 SQL 注入
攻击
。然后,将 SQL 语句和相关参数传递给数据库驱动的查询执行方法。
数据库驱动会将查询发送到数据库,获取结果,并将其返回给你的应用程序。通常,返回的数据会以对象数组的形式表示查询结果。
下面是一个使用 better-sqlite3 驱动操作 SQLite 数据库文件的基本示例:
import Database from "better-sqlite3";
const db = new Database("chinook.sqlite");
const selectAlbumByID = "SELECT * FROM Album WHERE AlbumId = ?";
const row = db.prepare(selectAlbumByID).get(1);
console.log(row.AlbumId, row.Title, row.ArtistId);
既然已经了解了原始 SQL 的工作原理,那么让我们深入探讨一下仅使用它来与 SQL 数据库进行交互的优缺点。
👍 纯 SQL 的优势
使用纯 SQL 查询的最大好处之一是它提供了高度的透明度和控制力。通过直接编写 SQL 语句,你可以完全掌握数据库的每一个操作,清楚地了解数据的存储、结构以及检索方式。
这种直接操作方式减少了抽象层带来的意外情况,让你能够编写高度优化的查询,避免使用自动化查询工具时可能出现的低效问题。在需要执行复杂数据查询或数据操作的情况下,手动优化 SQL 语句尤其重要。
此外,纯 SQL 具有极高的灵活性,因为它不会受到任何抽象层的限制。你可以充分利用数据库引擎的所有功能,执行复杂的数据库特定查询,而这些查询在高级抽象工具中可能不受支持或难以实现。
最后,使用纯 SQL 还能帮助你深入理解 SQL 及数据库的运行机制。这对开发者来说是非常宝贵的知识,尤其是在性能优化和数据库管理方面。
👎 纯 SQL 的劣势
尽管纯 SQL 具有诸多优点,但它也存在一定的挑战。其中一个主要问题是编写和维护查询的复杂性。SQL 语句在处理复杂关系、嵌套查询或跨多个表检索数据时,往往会变得冗长且难以管理。
另一个问题是学习曲线较陡,特别是对于 SQL 经验不足的开发者来说。在 Node.js 生态系统中,开发者普遍更倾向于使用 ORM(对象关系映射)、查询构建器等抽象工具,因此,与这些广泛采用的方法相比,寻找纯 SQL 相关的资源和支持可能会更困难。
【早阅】Node.js 模块简史:cjs、打包工具和 esm
此外,直接编写 SQL 还可能带来安全隐患,例如 SQL 注入。如果没有正确处理查询,攻击者可能会利用漏洞执行恶意 SQL 语句。因此,开发者需要手动使用参数化查询或预处理语句,确保所有用户输入都经过严格的安全处理,以防止数据库被恶意操纵。
最后,使用原始 SQL 通常涉及将查询作为普通字符串进行操作,这可能会导致一些细微的错误(例如列名中的拼写错误或数据类型不正确),这些错误可能直到运行时才会被发现。如果你使用
TypeScript
,可以尝试
PgTyped
这样的工具,它能够在应用中以
类型安全
的方式使用原生 SQL,减少此类错误的发生。
适合使用纯 SQL 的场景
纯 SQL 适用于
对性能优化和细粒度控制要求极高
的场景,或者需要执行复杂的
非标准查询
(ORM 难以处理的情况)。如果你的项目涉及
复杂的数据操作
,或者需要充分利用数据库特定的功能,那么直接使用 SQL 可能是更好的选择。
了解查询构建器(Query Builders)
与直接编写 SQL 语句不同,你可以选择
查询构建器
(Query Builder)来与数据库交互:
📷
Knex.js 代码示例
查询构建器提供了一种更结构化、更安全的方式来构建查询,同时减少了手动拼接 SQL 语句的复杂性。
通常,查询构建器会提供一个
链式 API
,让你能够逐步构建复杂的查询。这种方式不仅能减少 SQL 注入等安全漏洞,还能简化动态数据的处理,使查询更易读、更易维护。
查询构建器的优势
查询构建器的最大特点是
在抽象与控制之间找到平衡
。你依然在操作
表、列、关系
等数
据库概念,但语法更贴合 JavaScript 语言习惯。这不仅提升了开发体验,也在一定程度上增强了代码的安全性和可读性,同时不会完全失去对数据库底层操作的理解。
Knex.js:Node.js 生态中的热门选择
Knex.js
是 Node.js 生态中非常流行的查询构建器,在 GitHub 上拥有
19k+ 颗星
,每周下载量
超过 170 万次
。
要使用 Knex.js,你需要安装
knex
包,并根据你的数据库类型安装对应的数据库驱动(如
pg
、
mysql
等)。
npm install knex sqlite3
你现在可以编写如下查询:
import knex from"knex";
const Database =knex({
client:"sqlite3",
connection:{
filename:"./chinook.sqlite",
},
useNullAsDefault:true,
});
const selectedRow =awaitDatabase("Album")
.where({
AlbumId:1,
})
.select("*");
console.log(selectedRow);
如果你主要编写像上面那样的简单、静态查询,那么查询构建器相对于原生 SQL 的价值可能并不明显。但在需要构建带有动态条件的查询时,它们的价值会迅速显现出来:
let query =knex("users");
if(searchCriteria.name){
query = query.where("name","like",`
%${searchCriteria.name}%`);
}
if(searchCriteria.email){
query = query.where("email", searchCriteria.email);
}
if(searchCriteria.minAge){
query = query.where("age",">=", searchCriteria.minAge);
}
await query.select("*");
Knex 的链式方法允许根据运行时条件轻松构建复杂的查询。相比之下,在原始 SQL 中,通过字符串拼接实现相同的结果不仅不方便,而且容易导致安全漏洞。
👍 查询构建器的优点
我们已经提到过一些理由来选择查询构建器而不是原生 SQL:它们简化了基于运行时条件构建动态查询的过程,并通过使用参数化查询来降低 SQL 注入攻击的风险。
另一个查询构建器的关键优势是,与原生 SQL 字符串相比,它们在长期维护中更具可维护性。通过使用熟悉的编程构造如链式方法,可以轻松地将复杂的查询分解为可管理的部分,并区分操作符和数据,同时保持 SQL 语义的忠实性。
knex("users")
.select("users.id", "users.name", "posts.title")
.join("posts", "users.id", "posts.author_id")
.where("posts.published", true)
.orderBy("posts.created_at", "desc");
与 ORM 不同,查询构建器还提供了对底层 SQL 查询的透明度。虽然它们使用方法来表示 SQL 原语,但底层的数据库操作并未被隐藏,因此任何熟悉 SQL 的人都可以理解查询的意图及其潜在的性能影响。
查询构建器通常还支持多个后端,允许你编写更适用于不同数据库系统的代码。虽然数据库后端在应用程序进入生产阶段后很少更改。然而,这一特性允许使用不同数据库的开发人员避免学习为每个数据库编写 SQL 的新范式。
虽然有人批评查询构建器没有抽象足够的 SQL 复杂性,但我认为这是它的优点。完全依赖于绕过学习 SQL 的工具在长远来看是有害的。
查询构建器若要有效使用,仍然需要对 SQL 原理有基础的理解。它们提供了一个结构化、安全的环境来编写 SQL,以提高可维护性,同时不会牺牲核心 SQL 语义。
👎 查询构建器的缺点
与编写原始 SQL 相比,查询构建器的缺点并不多。即使在处理查询构建器不提供的抽象操作时,通常也有 “原始” 模式可以直接将查询发送到后端,绕过查询构建器的典型界面。
knex("users")
.select("*")
.where(knex.raw("(age > ? OR email LIKE ?)", [18, "%@gmail.com"]));
在性能方面,查询构建器通常与原生 SQL 保持一致,尽管在某些情况下,精心优化的手动 SQL 查询可能会在效率上略胜一筹。
主要的权衡在于与 ORMs 的对比。查询构建器由于其较少的抽象层次,需要更深入地理解 SQL 概念和模式管理。你将失去自动对象关系映射、模式迁移以及 ORMs 通常提供的减少样板代码等便利。
谁应该使用查询构建器?
查询构建器非常适合那些寻求原始 SQL 控制和 ORM 便利之间平衡的人。如果你希望在保持对底层 SQL 概念透明的同时,采用更结构化和可维护的方法,那么查询构建器是一个很好的选择。
如果你经常需要处理多个数据库,查询构建器还可以为你提供一个一致的接口来构建查询,无论底层系统是什么。这可以减少你在从一个项目切换到另一个项目时的上下文切换开销。当你需要访问特定数据库的功能时,可以使用
raw()
。
理解对象关系映射器
ORMs 通过提供高层次的抽象,弥合了面向对象编程与关系型数据库之间的差距:
它们以面向对象的方式呈现数据,显著减少了所需的样板代码量,从而加快了开发速度。
通过访问和操作数据,ORM 能够减轻手动编写 SQL 的需求。它们将面向对象的操作转换为数据库可以理解的 SQL 命令,从而使你能够更多地关注业务逻辑而非数据库的复杂性。
许多 ORM 也提供了管理数据库模式的内置功能,例如创建表、定义关系以及处理随着应用程序增长和发展而进行的模式迁移。
Node.js 中有许多 ORM 可供选择。Sequelize 是一个历史悠久的选择,但像 Prisma、MikroORM 和 Drizzle 这样的新选项正在流行起来,因为它们更注重开发人员的使用体验、类型安全和性能。
和查询构建器一样,你需要安装 ORM 及其相应的数据库驱动程序:
npm install sequelize sqlite3
安装完成后,您需要定义模型来表示数据库中的表和关系。之后,你可以使用 ORM 的方法与数据库交互 —— 执行查询、创建、更新或删除数据 —— 同时轻松管理模式。
import { DataTypes, Sequelize }from"sequelize";
const sequelize =newSequelize({
dialect:"sqlite",
storage:"chinook.sqlite",
});
const Album = sequelize.define(
"Album",
{
AlbumId:{
type: DataTypes.INTEGER,
primaryKey:true,
autoIncrement:true,
},
Title:{
type: DataTypes.STRING,
allowNull:false,
},
ArtistId:{
type: DataTypes.INTEGER,
allowNull:false,
},
},
{
timestamps:false,
tableName:"Album",
}
);
const album =await Album.findByPk(1);
console.log(album.AlbumId, album.Title, album.ArtistId);
简而言之,ORM 提供了一个强大的抽象层,简化了与数据库的交互。这使得开发者更容易管理复杂的关系,并自动化诸如模式迁移等繁琐任务,而在需要时仍然可以提供灵活性和控制。
👍 ORMs 的优点
ORMs 为 Node.js 开发人员使用数据库提供了许多优势。主要好处之一是它们提供的高级抽象层次,使得可以通过熟悉的面向对象概念而不是原始 SQL 来进行数据库交互。
这种抽象大大减少了所需的样板代码,并通过简化常见的数据库操作(如 CRUD 查询)来加快开发速度。它们内置的模式管理工具还有助于数据库版本控制和迁移,同时减少了错误的风险。
ORM 的另一个主要优势是它们能够管理不同实体之间的复杂关系。与手动在 SQL 中映射一对一或多对多关系不同,ORM 会自动处理这个过程,使得定义和程序化导航这些关系变得更加容易。
安全性是对象关系映射(ORM)表现出色的另一个领域。由于它们会自动处理查询构建,因此默认情况下会缓解常见的安全漏洞,如 SQL 注入,确保用户输入能够安全处理和净化。
在可移植性方面,ORM 提供了一层与数据库无关的交互,这意味着你可以通过最少的代码更改在不同的数据库系统之间切换。
最后,许多现代 ORM 提供了强大的类型安全性和性能优化,这有助于开发人员早期捕获错误并优化数据库交互,特别是与 MikroORM 和 Drizzle 等 newer 工具一起使用时。
👎 ORM 的缺点
尽管 ORM 提供了强大的抽象,但也存在一些明显的挑战。
一个主要问题是性能开销,因为 ORM 经常生成低效的查询(即使是简单的任务),这是由于其广泛的设计旨在适应各种各样的使用场景。这可能导致性能显著下降,特别是在高流量应用或处理复杂数据操作时。
另一个缺点是失去了对数据库操作的细粒度控制。虽然 ORM 在简化常见任务方面表现出色,但在优化某些查询、建模复杂关系或利用高级数据库特定功能时,可能在 ORM 的限制内面临挑战。
这些挑战往往源于应用程序代码中使用的面向对象范式与关系数据库基于表的性质之间的摩擦(即对象 - 关系 impedance 不匹配)。这种不匹配往往在管理数据库中的复杂关系时需要工作 - around 和妥协。
此外,ORMs 也有它们自己的学习曲线。你需要理解 ORM 的特定语法、约定以及它如何将对象映射到数据库表。如果你已经熟悉 SQL,适应 ORM 的范式有时会感觉像是额外的一层复杂性,因为你现在必须同时处理数据库和 ORM 的内部逻辑。
谁应该使用 ORM?
对象关系映射(ORM)通常非常适合那些将开发速度置于运行时性能之上的情况。它们也非常适合构建复杂查询很少而增删改查操作常见的应用程序。
探索一种混合方法
在本文中,我们主要将选择 原生 SQL、查询构建器和 ORM 之间的选择视为非此即彼的选择。然而,在实际上,你可以通过结合多种方法的优点来采用混合策略,以适应手头的项目。
一种常见的混合策略是使用 ORM 处理大部分数据访问,同时使用原生 SQL 来处理性能关键查询或利用 ORM 不易支持的特定数据库功能时使用原始 SQL。
例如,你可能使用 ORM 来处理标准的 CRUD 操作和模型关系,但在进行复杂连接、聚合或专门的数据库操作时切换到原生 SQL。
另一种变体是将查询构建器作为数据库操作的主要接口。这可以提高可维护性和组合性,同时保留 SQL 语义,并允许在需要时切换到原生 SQL。
混合方法可以让你兼得两者之长:你可以根据需要选择某种抽象层带来的便利,同时在需要时也能获得直接数据库访问的性能和控制。
总结
在 Node.js 生态系统中,当涉及到数据库交互时,并没有一种放之四海而皆准的答案。原始 SQL 提供了无可比拟的控制力和性能,但需要专业知识。查询构建器提供了便利性和灵活性的平衡,而 ORM 则侧重于抽象和快速开发。
最终,最佳选择取决于项目的具体需求、团队的专业水平以及你愿意接受的权衡。无论你选择哪条路径,对 SQL 的扎实掌握仍然是有效管理关系数据库的基础。
关于本文
译者:@飘飘
作者:@Damilola Olatunji
原文:
https://blog.appsignal.com/2025/03/26/how-to-choose-between-sql-query-builders-and-orms-in-nodejs.html
😀 每天只需花五分钟即可阅读到的技术资讯,加入【早阅】共学,可联系 vx:zhgb_f2er
5 分钟新知:了解外面世界的一种方式。
🚀可直接通过阅读原文了解详细内容。
这期前端早读课
对你有帮助,帮”
赞
“一下,
期待下一期,帮”
在看
” 一下 。