1. TypeScript 后端的数据访问层,到底烂在哪儿了
这几年只要一提到"重写后端",团队里最先达成的共识几乎都是:别再让 Prisma 那套自创语义继续往上叠业务了。TypeScript 本身是一个静态类型系统非常强的语言,按理说从 API 入口到数据库边界,类型应该一路畅通。真做起来却是另一个局面:绝大多数 ORM 在数据访问层都搞了一套自己的运行时魔法,类型安全在最后这一百米断档,导致你在接口层写得很爽的 interface,一落到数据库交互就变成了 any 或者一堆靠猜的字段。
1.1 ORM 的重型运行时,是一次性的"魔法税"
先别急着杠,我并不是说 ORM 没有价值。项目早期用 ORM 快速建模、自动迁移,确实能把效率拉满。但当你维护一个两年以上的 TypeScript 后端,ORM 的累积成本会呈指数上升。
以 TypeORM 为例,它是典型的装饰器 + 反射方案。实体类上挂满 @Entity、@Column、@ManyToOne,运行时要靠 reflect-metadata 去解析这些装饰器,然后把类实例映射成数据库行。这套机制在 JavaScript 时代还能接受,但在 TypeScript 后端里就很别扭:你的数据库结构被"藏"在类装饰器和运行时元数据里面,IDE 跳转看到的是一堆装饰器工厂,类型推断偶尔还要靠 FindOptionsWhere<T> 这种半吊子泛型兜底。
Prisma 是另一个方向的典型。它通过 schema.prisma 声明模型,然后跑 prisma generate 生成客户端代码。思路很干净:模型文件里写清楚表结构,生成的客户端带着完整类型。问题是它的二进制引擎和生成代码都很重,每次 Schema 一改就要重新 generate,CI 里必须多一步生成流程,换机器、换 Node 版本、跑 Docker 构建时经常莫名踩坑。更麻烦的是,Prisma 的自创查询语法偏离 SQL 原义,遇到复杂的 join、子查询、窗口函数,你得上 $queryRaw 写原生 SQL,等于绕回起点。
你会发现这两种主流方案都躲不开同一个问题:**它们都在你的代码和数据库之间垫了一层"翻译",而这层翻译本身还要维护,还要升级,还会出 bug。**我把这称之为魔法税,它不是一次性交完,而是每年都要续费。
1.2 类型安全在数据库这头断档,后端代码就失去了一半价值
TypeScript 后端最值钱的东西是什么?我的答案非常朴素:把能静态检查的错误尽量在编译期拦下来。请求参数校验、业务逻辑判断、数据组装,这些都是前端可以理解的层面。但真正危险的往往是数据库边界——字段名拼错、类型不匹配、可空性判断失误,这些错误通常不会在编译期暴露,而是等到运行时返回 undefined 或者抛出一个模糊的数据库驱动报错。
传统 ORM 在这一点上的表现参差不齐。TypeORM 的查询结果类型基本靠泛型和实体类推导,看着有类型,其实在复杂关联查询里经常是 any 或者变成联合类型的噩梦。Prisma 比 TypeORM 好很多,生成类型的精确度确实高,但它的类型系统是"代码生成出来的二次类型",一旦你写 prisma.user.findMany() 并且没有把 include 和 select 写对,IDE 给你的报错信息可能要花十分钟才能看懂——因为那是在一堆由 generate 产生的 .d.ts 里推出来的错误。
在我接触 Drizzle ORM 之前,我一直以为自己只能在这两种方案里选一个凑合。后来看到一个观点让我印象很深:**Drizzle 不是又一个 ORM,它是把 SQL 本身作为类型推导的核心,让 SQL 的每一段都变成 TypeScript 可表达的 AST。**它不是去"封装" SQL,而是去"复刻" SQL 的类型结构。这意味着类型从你的代码一路流到 SQL 语句再回到查询结果,几乎不会变形。
1.3 为什么我这时候开始认真看 Drizzle
我是从一次重构开始彻底转向 Drizzle 的。当时项目里用的 Prisma,Schema 文件四十多个模型,编译产物客户端超过 8MB,每次部署构建都要重新生成。为了排查一个诡异的关联查询,我不得不打开 Prisma 生成的内部类型文件去追字段映射。折腾到凌晨三点,我意识到这个方向有问题:如果 ORM 让我越来越远离 SQL 和数据库本身,而我又必须在生产环境里面对真实的 SQL 行为和数据库特性,那我为什么要多付一层高昂的维护成本?
于是我开始拿小项目试 Drizzle。试用两周后,感受可以用四个字概括:手脚清爽。它没有定义一套全新的查询世界,它的核心 API 就是 select、insert、update、delete,语义上跟 SQL 一一对应,只是补上了 TypeScript 推断。然后我很快把简历里能写 "熟练使用 Prisma" 的地方全都改掉了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Drizzle 的底层思路:它没想取代 SQL,它想给 SQL 配上 TypeScript 类型
很多第一次接触 Drizzle 的人会问:这玩意儿到底是 ORM 还是 query builder?我的理解是:它两个都是,但它最本质的定位更接近"SQL 的类型安全表达式"。
2.1 headless schema:零代码生成,零重复依赖
先聊架构层面最特别的设计——headless。这个词听起来玄乎,翻译成大白话就是:Drizzle 的核心运行时不依赖任何自动生成的代码,也不依赖任何 Prisma engine 那样的二进制进程。你需要做的事情非常简单:在 TypeScript 里用 pgTable() 定义好表结构,然后直接调用 db.select() 查询。
对比一下就清楚了。Prisma 的工作流程是:定义 schema.prisma → 跑 generate → 生成客户端 → import 客户端 → 查询。一旦你的开发机器和 CI 环境里的 generate 步骤出问题,或者两个环境的 Prisma 版本不一致,你连代码都跑不起来。Drizzle 没有这一步,因为表定义本身就是类型和运行时共用的真源。你写:
typescript复制import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
这一个文件既能在运行时用来拼接 SQL,又能让 TypeScript 推导出 typeof users.$inferSelect 和 typeof users.$inferInsert 作为查询结果的类型。没有额外代码生成,没有重复的类型定义,没有要从某个隐藏目录 import 的 client。你的表和你的类型在同一个文件里,改一处,另一处自动跟着变。
这一点带来的工程收益是巨大的。代码 review 的时候,一个 PR 里如果改了 users 表的字段定义,IDE 能立刻高亮出所有使用到旧字段的查询语句,因为类型系统已经感知到了。这在 Prisma 里也能做到,但前提是你得保证所有开发者都记得
