首页 / 文章

原始 SQL、Prisma 或 Drizzle:为类型安全与控制选择数据库层

Raw SQL、Prisma 与 Drizzle 在 PostgreSQL 上在控制能力、类型安全以及开发者体验方面有何差异,以及为 TypeScript 应用选择其中一种工具的实际方法。

发布者:REACTAPP.TOP阅读约 15 分钟3403 词

来源: Prisma vs Drizzle vs Raw SQL: What Every Backend Developer Should Understand Before Choosing an ORM

大多数后端开发都是从获取 API 响应开始的。你需要配置路由、添加一两个控制器,再接入认证机制和一些中间件,初期工作大致就这些。但随后会出现一个更棘手的问题:应用程序究竟该如何与数据库通信?顿时会出现一大堆术语(原始 SQL、驱动程序、ORM、查询构建器、Prisma、Drizzle),而且各方观点纷杂。有些工程师绝不使用 ORM,另一些人则称 Prisma 为他们节省了数周时间,还有一部分人选择 Drizzle,因为他们希望保持 SQL 的可见性而非将其隐藏。以下内容将从基础原理逐步展开讨论,这样在 TypeScript 和 PostgreSQL 技术栈中选择原始 SQL、Prisma 还是 Drizzle 时,就变成了关于控制能力和类型安全性的技术决策,而非跟风选择。

请求如何到达数据库

在了解任何工具名称的含义之前,先了解请求在典型后端系统中的处理流程会很有帮助。有人访问您的网站并点击“显示我的个人资料”之类的控件。该操作始于浏览器,随后通过您的 API 传递,进入数据库层,最终到达数据库本身,之后才会返回响应。

Browser
   |
   vExpress API   |
   vDatabase Layer   |
   vPostgreSQL Database   |
   vResponse back to the user

中间的数据库层正是容易产生混淆的地方。您的应用程序需要某种机制与数据库进行通信,目前有三种常见方式:手动编写原始SQL语句、使用Prisma这样的ORM工具,或是使用Drizzle这样的查询构建器。以下各节将依次介绍这三种方法。

手动编写SQL

最直接的方法也是最传统的方法:由您自己编写SQL语句。想象一个包含常见列的简单用户表。

users
id
name
email
password

获取单个用户的操作极其简单。

SELECT *
FROM users
WHERE id = 1;

由于数据库以 SQL 作为其原生语言,数据在进出时无需进行任何翻译。在基于 PostgreSQL 的 Node.js 环境中,通常使用 pg 包来直接发送查询语句,可通过该包导入连接池。

import { Pool } from "pg";

然后通过环境变量配置数据池,并对其执行参数化查询,读取查询结果中的行数据。

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});const result = await pool.query(
  "SELECT * FROM users WHERE id = $1",
  [1]
);console.log(result.rows);

你的代码会发送SQL语句,PostgreSQL负责执行它,数据会直接返回,中间没有任何抽象处理。自己编写SQL能让你完全掌控流程。例如,复杂的报表功能可以直接实现,比如将用户与他们的订单关联起来,再通过分组统计每位用户的订单数量。

SELECT
users.name,
COUNT(orders.id)
FROM users
JOIN orders
ON users.id = orders.user_id
GROUP BY users.name;

对于如此复杂的分析查询,手写 SQL 真的很难有更好的替代方案。编写 SQL 还能迫使你理解连接、索引、执行计划以及整体性能,而这些知识无论日后使用何种工具都依然有用。此外,代码与数据库引擎之间也没有额外的中间层。

一旦项目中的查询数量超过少数几个,缺点就会显现出来。想象一下代码库中散布着数百条这样的查询,其中很多都是类似通过电子邮件进行查找的那种查询。

const result = await pool.query(
`
SELECT *
FROM users
WHERE email=$1
`,
[email]
);

现在假设有人重命名了某个列,将 email 改为 email_address。任何涉及该列的查询都会无声无息地出错。数据库会立即记录这一变更,但你的 TypeScript 代码在运行时出现故障之前根本不会察觉。还存在同一风险更为隐蔽的表现形式。比如你获取第一行数据的情况。

const user = result.rows[0];

然后你从中读取一个属性,结果出现了拼写错误。

console.log(user.emial);

拼写错误的 emial 不会引发 JavaScript 的任何错误提示。只有当该行代码被执行并在用户面前出错时,你才会发现这个问题。正是这类问题促使 ORM 的诞生。

ORM 的真正含义

ORM 即对象关系映射,虽然听起来很宏大,但实际上只是指在编程语言与数据库之间建立了一座桥梁。你无需手动编写这样的查询语句,

SELECT *
FROM users
WHERE id=1;

你应该写成更接近对象方法调用的形式。

user.findUnique({
 id: 1
})

ORM 会处理这些信息并在后台生成真正的 SQL 语句,这样你的代码就会通过 ORM 传递到生成的查询语句,最终发送到数据库中。

Your TypeScript code
        |
        vORM        |
        vSQL query        |
        vDatabase

团队选择 ORM 是因为现代应用程序需要的不仅仅是发送查询的功能。它们需要类型安全、真正的数据迁移功能、无需记住每张表结构的模式管理方式,以及更少的重复性样板代码。在当今的 TypeScript 环境中,最常被提及的 ORM 工具有 Prisma、Drizzle、TypeORM 和 Sequelize。此处重点介绍原始 SQL、Prisma 和 Drizzle,因为这三种工具涵盖了大多数现代 TypeScript 后端实际正在考虑的选项。

了解 Prisma

Prisma 是 TypeScript 生态系统中最受关注的 ORM 之一,每当谈论现代 Node.js 后端时,它往往会是首先被提及的名字。它的核心作用是让 TypeScript 应用能够通过普通的 TypeScript 代码与数据库进行交互,而无需到处使用 SQL 语句;它支持 PostgreSQL、MySQL、SQLite、SQL Server 和 MongoDB。你只需编写 TypeScript 代码,Prisma 便会将其转换为查询语句。

TypeScript code
        |
        vPrisma Client        |
        vSQL queries        |
        vDatabase

典型的 Express、Prisma 和 PostgreSQL 架构层次分明:请求通过 API 进入,经过服务层处理,最终通过 Prisma Client 连接到数据库。

Client
  |
  vExpress API  |
  vService Layer  |
  vPrisma Client  |
  vPostgreSQL Database

Prisma Client才是真正执行任务的部分:它是你的后端代码与其后端数据库之间的桥梁。一切始于一个名为schema.prisma的文件,该文件将整个数据库结构集中描述在其中。例如,一个用户模型就会定义四列带类型的信息。

model User {
  id       Int    @id @default(autoincrement())
  name     String
  email    String @unique
  password String
}

该代码块告知 Prisma 使用这些列创建一个 User 表,Prisma 能准确理解其含义。而在原始 SQL 中,则需要明确的语句来创建相同的表。

CREATE TABLE users (
 id SERIAL PRIMARY KEY,
 name TEXT,
 email TEXT UNIQUE,
 password TEXT
);

Prisma只需要上述的架构定义块,便会通过迁移操作将其转换为实际的数据库变更。迁移本质上就是记录数据库随时间变化的历程。假设你的用户表最初结构非常简单。

User
id
name
email

几个月后,您想要添加电话号码。无需手动修改表格,只需执行一条命令即可。

npx prisma migrate dev --name add_phone_number

Prisma 会生成迁移文件来记录这些变更,因此该文件夹会从最初的初始化迁移文件逐渐变为包含电话号码字段的迁移文件,所有的变更历史都会被版本化管理,而不会仅存在于某人的记忆中。一旦架构定义完成,Prisma 会生成可在代码中使用的客户端,例如用于根据 ID 获取用户信息。

const user = await prisma.user.findUnique({
  where:{
    id:1
  }
});

那些在幕后悄然变成普通 SQL 的过程。

SELECT *
FROM users
WHERE id=1;

你无需亲自编写 SQL 代码。日常操作也能直观对应:插入操作变为 create 调用,获取所有数据变为 find-many 调用,更新记录则通过 where 子句作为键进行 update 调用,删除操作则为 delete 调用。Prisma 最突出的优势在于类型安全。如果你的架构定义了包含 id、name 和 email 字段的 User 类型,那么在查询时每个字段都会提供真正的自动补全功能。

const user = await prisma.user.findMany();
user[0].email;

如果误将其写成 user[0].emali,Prisma 结合 TypeScript 会在应用程序运行之前就发现错误,这能大大节省后续的调试时间。当然,实际应用中很少只有单一表格。比如某个用户拥有多篇帖子,这时就需要分别处理用户相关的数据和帖子相关的数据,二者通过用户 ID 建立关联。

User
id
namePostid
title
userId

Prisma 在模式中直接体现了这种关系,既体现在用户模型上,

model User {
 id Int @id @default(autoincrement())

以及通过关联关系指向其用户的帖子模型。

 name String posts Post[]
}model Post { id Int @id @default(autoincrement()) title String userId Int user User @relation(
 fields:[userId],
 references:[id]
 )}

这样,获取用户及其帖子的操作就会变成一个包含相关记录的、易于理解的单一调用。

const user = await prisma.user.findUnique({
 where:{
  id:1
 },
 include:{
  posts:true
 }
});

这样就会返回一个包含帖子嵌套数组的用户对象,正是应用程序代码中所需要的结构。Prisma的优势在于:开发者使用体验极为流畅,在TypeScript项目中具备强大的类型安全性,数据库迁移过程井然有序,文档质量很高,而且整个团队能够快速阅读架构文档并理解数据库结构。

它并非没有代价。与手写的SQL相比,功能复杂的报表生成方式可能会显得不够便捷。由于Prisma能如此有效地隐藏SQL代码,那些只学习过Prisma的开发者日后在需要对数据库层面进行优化时往往会遇到困难。从技术层面来看,请求路径上多了一个环节,即从应用程序到Prisma,再到SQL,最后才是数据库,而非更简短的从应用程序直接到SQL再到数据库的路径。对于大多数应用程序而言,这额外的层次并不会造成问题,但了解它的存在仍是必要的。Prisma在构建TypeScript API、追求快速开发、主要需要标准的创建-读取-更新-删除操作,以及非常重视开发者体验的场景下表现最为出色,这也是许多初创企业和SaaS产品倾向于使用它的原因。

Drizzle的适用场景

Drizzle是另一款在TypeScript领域发展势头强劲的ORM框架,要理解它首先需要认识到Prisma与Drizzle基于截然不同的设计理念。Prisma力求尽可能让开发者远离数据库的复杂性;而Drizzle则采取相反的策略:让开发者更贴近SQL语法,仅在之上添加TypeScript的安全性保障。尽管两者目标一致,即提升数据库操作体验,但实现路径截然不同。Drizzle是一款轻量级的TypeScript ORM,支持PostgreSQL、MySQL和SQLite,其设计重点在于类型安全、高性能、贴近SQL语法以及保持简洁的架构。开发者仍然使用TypeScript编写代码,这些代码会通过Drizzle转换为查询语句并发送到数据库中。

TypeScript code
        |
        vDrizzle ORM        |
        vSQL query        |
        vDatabase

将两者放在一起就能明显看出其中的哲学差异。在 Prisma 中检索所有用户的方式如下,而其背后生成的 SQL 语句往往不会被人注意到。

const user = await prisma.user.findMany();

Drizzle希望实现更接近真实SQL的语法形式。

const users = await db
.select()
.from(usersTable);

几乎可以直接从代码中看出数据库层面的行为。架构也有所不同:Prisma将它们保存在专门的schema.prisma文件中,而Drizzle则通过纯TypeScript来声明架构,首先需要导入所需的列构建器。

import {
 pgTable,
 serial,
 varchar
} from "drizzle-orm/pg-core";

并声明带有类型化列的表格。

export const users = pgTable("users", { id: serial("id").primaryKey(), name: varchar("name"), email: varchar("email")});

因此,你的数据库结构就直接存在于 TypeScript 代码库中,无需额外处理任何schema方言。日常操作看起来就像套上了 TypeScript 外衣的 SQL:插入操作就是带有参数的插入调用,读取所有数据则是从表中执行 select 操作,而过滤则通过等值运算辅助函数添加 where 子句。人们选择 Drizzle 有幾個原因:它真正轻量级,几乎没有抽象层;由于它贴近 SQL,你的 SQL 技能会不断精进,而不会被抽象层掩盖;它生成的查询往往具有可预测性,因为从你的意图到最终结果之间没有太多中间环节;此外,它还是为现代运行时环境设计的,包括边缘计算和无服务器环境。

另一方面,如果你从未真正学过 SQL,Drizzle 一开始可能会比 Prisma 更让人困惑,因为 Prisma 通常对新手更为友好。要熟练使用 Drizzle,确实需要理解连接、关系、索引以及查询的行为方式,没有任何可以回避的内容。虽然它的生态系统、文档和社区都很不错,但仍然比 Prisma 更成熟的环境要小。最简洁的对比方式是:使用 Prisma 时,你会把数据库视为一组对象,重点在于开发者体验;而使用 Drizzle 时,你更像是在使用 SQL 本身,重点则转向性能和控制。

三种方法的并列对比

将这三种方法放在一起看,它们的架构可以看作是应用程序与数据库之间层次数量的连续谱系。

Raw SQL:      Application → SQL Query → Database
Prisma:       Application → Prisma Client → SQL → Database
Drizzle:      Application → Drizzle → SQL → Database

以一个简单的操作为例:在基本的用户表中根据电子邮件查找用户。在原始 SQL 中,可以直接发送参数化查询。

const result = await pool.query(
`
SELECT *
FROM users
WHERE email=$1
`,
["rahul@test.com"]
);

在 Prisma 中,你可以调用唯一查找功能,

const user =
await prisma.user.findUnique({

在 where 子句中传递电子邮件。

where:{
 <email:"rahul@test.com>"
}});

在 Drizzle 中,你可以根据邮件列的相等条件来构建筛选条件。

const user =
await db
.select()
.from(users)
.where(
 eq(
 users.email,
 "rahul@test.com"
 )
);

最终,这三种方式都会返回相同的数据。不同之处在于你对控制程度的要求、能接受的抽象层次,以及你已掌握的 SQL 知识水平。初学者常犯的错误是只关注哪种方式最快,但实际上在项目中速度很少是决定性因素。更重要的问题是团队已经熟悉哪种技术、项目的真实复杂程度、每个人的数据库知识水平如何,以及查询可能会变得多复杂。对于简单的 SaaS 应用,仅使用 Prisma 通常就足够了;而那些对数据库依赖度很高的应用,则需要借助 Drizzle 或在某些地方直接使用原始 SQL 才能高效运行。

用通俗的话总结这些权衡:原始 SQL 的学习曲线最陡,Prisma 最容易上手,Drizzle 则处于中等水平。原始 SQL 对 SQL 的控制力最强,Drizzle 也较强,而 Prisma 最弱。原始 SQL 的类型安全性较差,但 Prisma 和 Drizzle 的类型安全性都很出色。在性能方面,原始 SQL 和 Drizzle 的表现极为优异,Prisma 也相当不错。开发者体验方面,Prisma 最佳,Drizzle 较好,原始 SQL 则处于中等水平。处理复杂查询时,原始 SQL 和 Drizzle 更为合适,Prisma 表现居中。至于对初学者的友好程度,Prisma 最领先,Drizzle 处于中等,原始 SQL 最落后。

那么你到底该选哪个呢?

ORM 是否应始终作为默认选择?老实说,并非如此。与其他工具一样,ORM 在某些任务中表现优异,但在其他任务中则不适用,没有哪种工具能适用于所有项目。当需要最大程度的控制、应用程序对数据库的依赖度极高、查询变得复杂,或者性能容不得任何下降时,原始 SQL 才是最佳选择。分析平台、报表系统以及金融应用都属于这类场景,任何需要处理大量数据的场景也都适用。比如,要生成一份报告来显示每位客户五年内的购买金额及其平均订单价值,直接使用 SQL 通过聚合函数来处理会更为简便。

SELECT
customers.name,
COUNT(orders.id),
AVG(orders.amount)

并通过连接和分组来生成它们。

FROM customersJOIN ordersON customers.id = orders.customer_idGROUP BY customers.name;

对于这类工作,确实没有什么能替代SQL的表达能力。问题在于,在庞大的代码库中凡事都依赖手写的SQL,很快就会变得难以管理,结果就是会有数百个查询文件散落在各处,而且类型安全问题也需要自己不断监督。通过Node.js API性能的优先级视图可以看出,真正的优劣往往取决于数据库而非框架本身。

在构建 TypeScript 后端、主要处理标准的创建/读取/更新/删除操作、注重开发效率以及需要多个开发者在同一代码库中协作时,Prisma 通常是更佳选择。这类场景常见于 SaaS 产品、管理控制台、店铺 API 以及内容管理系统,日常任务多为用户注册、资料更新、下订单或获取控制台数据。Prisma 能真正提升工作效率,将原始的查询操作转化为结构清晰、易于理解的调用方式,任何团队成员都能一目了然地理解。而 Drizzle 则适合那些已经熟练掌握 SQL、注重性能与控制力、希望使用轻量级而非重型解决方案的 TypeScript 开发者,它能为开发者提供 ORM 的安全保障,同时避免与数据库产生过大的距离感。

在实际应用中,许多真实项目并不会坚持采用单一方案。一家初创公司很可能会选择从 Express 开始,依次经过 Prisma 最终连接到 PostgreSQL 的路径。

Express → Prisma → PostgreSQL

这确实是第一步中相当明智的选择。后来,随着产品的发展以及更复杂的分析或报表需求出现,团队往往选择在某些部分直接使用原始SQL,而非彻底放弃Prisma,结果就是应用程序既通过Prisma又通过手写的查询语句来访问PostgreSQL。

Application
├── Prisma
└── Raw SQL Queries
        ↓
   PostgreSQL

并非必须处处采用同一种方法,大多数实际代码库最终都会混合使用多种技术。初创公司往往为了提升效率而选择 TypeScript、Prisma 和 PostgreSQL。较新的 TypeScript 项目则越来越多地采用 Drizzle 及其轻量级、对 SQL 友好的设计风格。而大型系统则常常将 ORM 与原始 SQL 以及专门的数据库优化手段结合使用,因为大型系统的不同部分会面临不同的问题。

应避免的错误

有一些常见的错误会反复出现。首先是把 ORM 当作数据库来使用。Prisma 和 Drizzle 都不是数据库,它们只是用于连接底层数据库的工具,无论是 PostgreSQL、MySQL 还是 MongoDB。第二种错误是完全跳过 SQL,直接使用 Prisma。起初这看似提高了效率,但一旦查询变慢且难以解释、连接操作变得异常复杂,再加上缺乏底层基础导致调优变得极为困难,就会带来麻烦。ORM 从来就不是为了取代 SQL,而是建立在 SQL 之上的。第三种错误则是过分追求原始性能指标。在现实工作中,开发效率同样重要,如果为了节省五毫秒的查询时间而让开发周期延长三个月,那通常并非明智之举。

值得投入的学习路径

从今天开始,合理的学习顺序是先掌握 SQL 的基础知识,包括表、主键、外键、连接、索引以及事务,可以使用 PostgreSQL 作为实践工具。接着通过 Node 的 pg 包等驱动程序直接进行连接操作,从而了解应用程序是如何直接与数据库通信的,中间没有其他隐藏环节。之后再学习 Prisma,熟悉其模式设计、数据迁移、关联关系以及类型安全功能。最后,尝试使用 Drizzle,体验以 SQL 为核心的查询构建器的使用方式,以及它所带来的性能权衡。

学习后的进展

ORM的存在目的从来不是取代SQL,而是为了提升开发者的体验,没有比这更重要的了。一个真正出色的后端开发者并非取决于是否精通Prisma或Drizzle,而是那些了解数据库实际运作原理、能在需要时编写SQL、明白ORM在哪些方面为他们简化工作,并且同样重要的是,知道何时该使用ORM、何时手写SQL才是更明智的选择的人。一旦具备这种清晰的认识,选择这些工具就不再需要猜测,而是根据项目的实际需求来决定,而非跟随当月的流行趋势。

相关阅读

  • 在购买更大数据库之前降低 Prisma 和 PostgreSQL 的负载:介绍了十五种实用技巧,从 EXPLAIN ANALYZE 和复合索引到 N+1 问题解决方案、计数器、连接池以及真空操作,旨在减少 Prisma 数据库的操作量。