Node

Node.js ORM 选型实战:Prisma、Drizzle、TypeORM 与 Sequelize 深度对比指南

✎ -- 字 🕐 -- 分钟
字号

Node.js ORM 对比封面

Node.js ORM 选型实战:Prisma、Drizzle、TypeORM 与 Sequelize 深度对比指南

一、ORM 的价值与代价:什么时候该用

写裸 SQL 的痛谁都知道:拼接字符串容易注入、字段类型全靠脑子记、改表结构后代码不报错但运行炸了。ORM 把数据库表映射成编程语言对象,让你用代码而不是 SQL 字符串来操作数据。但 ORM 不是银弹——它引入了一层抽象,复杂查询可能被翻译成低效 SQL,N+1 查询问题几乎每个新手都会踩。关键不是"要不要用 ORM",而是"在什么场景下用哪个 ORM"。

维度 裸 SQL (mysql2/pg) 查询构造器 (Knex) ORM (Prisma/Drizzle 等)
开发效率
类型安全需手动自动生成
SQL 灵活度100%80%60-90%
性能控制完全可控较好有隐藏开销
迁移管理手动内置内置

二、四大 ORM 概览与定位

先说结论,这四个方案的设计哲学完全不同:

  • Prisma:Schema 优先,用 .prisma 文件定义模型,自动生成 TypeScript 类型。查询 API 接近自然语言,适合追求开发效率和类型安全的中大型项目。
  • Drizzle ORM:SQL 优先,API 设计贴近原生 SQL 语法,极轻量、零依赖运行时。适合需要精细控制 SQL 和追求极致性能的场景。
  • TypeORM:装饰器优先,像 Java JPA / Hibernate,大量使用 TypeScript 装饰器。适合 NestJS 生态和从 Java/.NET 背景迁移的团队。
  • Sequelize:老牌选手,API 稳定但偏向旧风格。生态成熟但类型支持较弱,适合维护遗留项目。

核心特性速览

特性 Prisma Drizzle TypeORM Sequelize
Schema 方式.prisma DSLTS Schema 函数装饰器define() 调用
类型生成codegen推导反射手动/推导
运行时依赖~5MB Engine零依赖reflect-metadata内置
迁移工具prisma migratedrizzle-kitCLI + Entitysync/umzug
支持数据库PG/MySQL/Mongo等PG/MySQL/SQLitePG/MySQL/Mongo等PG/MySQL/MSSQL等
GitHub Stars40k+25k+30k+29k+
适合场景中大型应用高性能/EdgeNestJS 生态遗留维护

三、Schema 定义:建模方式对比

同样是定义一个 User 和 Post 的一对多关联关系,四个 ORM 写法差异很大。理解这些差异是选型的第一步。

3.1 Prisma:DSL 优先

// schema.prisma
model User {
  id    Int      @id @default(autoincrement())
  email String   @unique
  name  String?
  posts Post[]   @relation("UserPosts")
}

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  author   User?  @relation("UserPosts", fields: [authorId], references: [id])
  authorId Int?
}

Prisma 用独立的 .prisma 文件定义模型,不是 TypeScript 代码。运行 prisma generate 后自动生成完整的类型定义和查询客户端。这种"声明式建模"的好处是模型定义和数据操作解耦,缺点是引入了新的 DSL 语法学习成本。

3.2 Drizzle:SQL 贴近原生

// schema.ts
import { pgTable, serial, varchar, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 100 }),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: varchar('title', { length: 200 }).notNull(),
  authorId: integer('author_id').references(() => users.id),
});

Drizzle 用纯 TypeScript 函数定义表结构,类型自动推导,不需要额外 codegen 步骤。这种设计让 IDE 的跳转、重构、搜索全部可用,没有 Prisma 那层生成代码的隔阂感。对于熟悉原生 SQL 的开发者来说,Drizzle 的学习曲线最低。

3.3 TypeORM:装饰器风格

// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Post } from './post.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column({ nullable: true })
  name: string;

  @OneToMany(() => Post, (post) => post.author)
  posts: Post[];
}

TypeORM 用装饰器标注实体类,风格接近 Java JPA。需要 reflect-metadataemitDecoratorMetadata 两个 TypeScript 编译选项。好处是与 NestJS 的依赖注入体系完美融合,坏处是装饰器元编程增加了调试难度——类型报错信息往往不够直观。

3.4 Sequelize:函数调用

// models/user.js
const { DataTypes } = require('sequelize');

const User = sequelize.define('User', {
  email: { type: DataTypes.STRING, unique: true, allowNull: false },
  name: { type: DataTypes.STRING, allowNull: true },
}, { tableName: 'users' });

const Post = sequelize.define('Post', {
  title: { type: DataTypes.STRING, allowNull: false },
});

User.hasMany(Post);
Post.belongsTo(User);

Sequelize 用 define 函数定义模型,关联关系通过 hasMany / belongsTo 等方法声明。这种方式上手快但 TypeScript 类型支持一直不够理想——虽然 v6 之后有所改善,但类型推断仍然经常需要手动标注,代码补全体验不如前三个方案。

四、查询 API:从 CRUD 到复杂关联

查询是 ORM 的核心战场。以下用同一个需求做对比:查用户列表,带每人的最新 3 篇文章,且只查 Gmail 用户。

4.1 Prisma 查询

const users = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    posts: {
      orderBy: { createdAt: 'desc' },
      take: 3,
      select: { id: true, title: true },
    },
  },
  where: { email: { contains: '@gmail.com' } },
  orderBy: { id: 'desc' },
});

Prisma 的查询 API 像嵌套 JSON,关联查询内联在 select 中,底层自动优化为 JOIN 或分批查询,天然避免 N+1。返回类型根据 select 精确推导,IDE 补全体验极佳。

4.2 Drizzle 查询

import { eq, desc, like } from 'drizzle-orm';

// Relational query API
const result = await db.query.users.findMany({
  with: {
    posts: {
      orderBy: desc(posts.createdAt),
      limit: 3,
      columns: { id: true, title: true },
    },
  },
  where: like(users.email, '%@gmail.com'),
  orderBy: desc(users.id),
});

// SQL builder mode
const rows = await db.select()
  .from(users)
  .leftJoin(posts, eq(posts.authorId, users.id))
  .where(like(users.email, '%@gmail.com'))
  .orderBy(desc(users.id))
  .limit(10);

Drizzle 提供两种查询模式:Relational API 类似 Prisma,SQL builder 模式更贴近原生 SQL。两种模式可以混用,复杂查询时用 SQL builder 获取精细控制,简单 CRUD 用 Relational API 提效。这种灵活性是 Drizzle 的核心竞争力。

4.3 TypeORM 查询

// find API
const users = await userRepository.find({
  where: { email: Like('%@gmail.com%') },
  order: { id: 'DESC' },
  relations: ['posts'],
  take: 10,
});

// QueryBuilder for complex cases
const users = await userRepository
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.posts', 'post')
  .where('user.email LIKE :email', { email: '%@gmail.com%' })
  .orderBy('user.id', 'DESC')
  .getMany();

TypeORM 的 find 在简单场景够用,但复杂关联查询通常要手写 QueryBuilder。QueryBuilder 更灵活但返回类型不如 Prisma/Drizzle 精确——getMany() 的返回类型是 User[],但实际可能包含关联数据,类型系统不感知。

4.4 Sequelize 查询

const users = await User.findAll({
  where: { email: { [Op.like]: '%@gmail.com%' } },
  order: [['id', 'DESC']],
  include: [{
    model: Post,
    limit: 3,
    order: [['createdAt', 'DESC']],
  }],
});

Sequelize 的 include 语法在简单场景下可读性不错,但嵌套查询和复杂筛选时配置项越来越多,加上类型支持弱,大项目里维护成本不低。

五、迁移管理:Schema 变更的版本控制

迁移工具决定了你改表结构时的心智负担。好的迁移工具应该能从模型定义自动生成迁移文件,而不是手写 SQL。这一维度上四个方案差距明显。

维度 Prisma Migrate drizzle-kit TypeORM CLI Sequelize CLI
自动生成diff 模型生成 SQLdiff 模型生成 SQLdiff Entity 生成 SQL手动写 migration
迁移文件.sql 文件.ts 文件.ts 文件.js 文件
种子数据prisma db seed自定义脚本自定义sequelize db:seed
回滚需手动 reset支持 down 迁移支持 down支持 undo
影子库检测

Prisma 迁移工作流

# 修改 schema.prisma 后生成迁移
npx prisma migrate dev --name add_user_avatar

# 生成迁移 SQL 并应用到开发数据库
# prisma/migrations/20240101000000_add_user_avatar/migration.sql 自动生成

# 生产环境部署(只执行已有迁移,不生成新的)
npx prisma migrate deploy

# 开发环境重置(危险!清库重建)
npx prisma migrate reset

Drizzle 迁移工作流

# 修改 schema.ts 后生成迁移
npx drizzle-kit generate --name add_user_avatar

# 迁移文件输出到 drizzle/ 目录,是 .ts 文件

# 应用迁移
npx drizzle-kit migrate

# 推送 schema 直接到数据库(开发阶段快捷方式)
npx drizzle-kit push

Prisma 和 Drizzle 都支持"改模型 -> 自动生成迁移 -> 应用"的工作流,极大减少了手写迁移 SQL 的负担。Drizzle 的迁移文件是 TypeScript,可以在其中加逻辑(如数据迁移)。Sequelize 在这维度最弱——很多迁移需要手写 updown 函数。

六、类型安全:TypeScript 集成深度

类型安全是现代 Node.js 项目选 ORM 的重要考量。四个方案的类型安全能力差异显著,直接影响开发体验和代码可靠性。

// Prisma: codegen 生成完整类型
const user = await prisma.user.findUnique({ where: { id: 1 } });
// user 类型: User & { posts: Post[] } | null  (精确到字段级别)

// Drizzle: 类型推导,零 codegen
const result = await db.select().from(users);
// result 类型: { id: number; email: string; name: string | null }[]  (自动推导)

// TypeORM: 依赖反射,关联类型可能不精确
const user = await repository.findOne({ where: { id: 1 } });
// user 类型: User | null  (关联类型需手动断言)

// Sequelize: 类型支持最弱,经常需要手动标注
const user = await User.findByPk(1);
// user 类型: User | null  (字段类型可能不精确)

Prisma 通过 codegen 生成精确到字段级别的类型,包括 select 后的部分字段类型都能正确推导。Drizzle 依赖 TypeScript 推导能力做到零 codegen 的类型安全,体验上与手写 TypeScript 几乎无异。TypeORM 的类型在简单场景够用,但关联查询时经常需要类型断言。Sequelize 从 v6 开始支持 TypeScript,但整体体验仍不够理想——类型推断不精确,泛型支持不完整。

七、性能实测:批量插入与关联查询

以下是在 PostgreSQL 16 + Node.js 22 环境下的简单基准测试(1000 条用户 + 每人 5 篇文章),仅供参考。实际性能受数据模型、查询复杂度、连接池配置等多因素影响。

操作 Prisma Drizzle TypeORM Sequelize 原生 pg
批量插入 1000 条~890ms~120ms~950ms~720ms~85ms
关联查询 100 用户~45ms~28ms~67ms~58ms~18ms
单条主键查询~2.1ms~1.5ms~3.2ms~2.8ms~0.9ms

注:Prisma 在 v5+ 引入了新的 Rust 执行引擎,批量操作性能有提升,但仍比 Drizzle 慢约 5-7 倍。Drizzle 因为极薄的抽象层,性能接近原生 pg 驱动。TypeORM 的开销主要来自反射和实体管理器。Sequelize 在 v7 重写了内核,性能有所改善但仍在测试中。

性能优化要点

// Prisma 批量插入:用 createMany 而非循环 create
await prisma.user.createMany({
  data: users.map(u => ({ email: u.email, name: u.name })),
  skipDuplicates: true,
});

// Drizzle 批量插入:直接传数组
await db.insert(users).values(userArray).returning();

// TypeORM:用 QueryBuilder 绕过 ORM 层
await userRepository
  .createQueryBuilder()
  .insert()
  .into(User)
  .values(userArray)
  .execute();

// Sequelize:bulkCreate
await User.bulkCreate(users, { ignoreDuplicates: true });

八、选型决策矩阵

综合以上维度,不同场景的推荐如下。如果你的项目还在犹豫选哪个,直接看最后两行:

ORM 决策矩阵

你的场景 推荐方案 理由
中大型 TS 项目,追求开发效率Prisma类型安全最强,生态成熟,文档好
高性能 / Edge Runtime / ServerlessDrizzle零依赖、极轻量、SQL 级控制
NestJS 项目 / Java 背景团队TypeORM装饰器风格与 NestJS 天然契合
维护遗留 Node 项目Sequelize不折腾,维持现状
全新项目,不确定用哪个Prisma综合体验最好,踩坑最少
极高性能敏感场景原生 pg/mysql2ORM 都有开销,绕过它

九、迁移实战:从 Sequelize 到 Prisma

如果你在考虑从旧 ORM 迁移到新方案,以下是一个常见的 Sequelize 到 Prisma 迁移路径,适用于想获得更好类型安全的团队:

# 1. 安装 Prisma
npm install prisma --save-dev
npx prisma init

# 2. 从现有数据库反向生成 schema
npx prisma db pull
# 这会扫描数据库表结构,自动生成 schema.prisma

# 3. 检查并补充关联关系
# db pull 不一定能正确推断外键关系
# 需要手动在 schema.prisma 中补充 @relation 声明

# 4. 生成 Prisma Client
npx prisma generate

# 5. 逐步替换查询代码(新旧并存阶段)
# 旧代码: const user = await User.findByPk(1);
// 新代码: const user = await prisma.user.findUnique({ where: { id: 1 } });

# 6. 迁移完成后移除 Sequelize 依赖
npm uninstall sequelize sequelize-cli

# 7. 将来变更走 prisma migrate dev 管理迁移

迁移的关键原则是"渐进式替换":不要一次性改完所有查询,而是按模块逐步替换,新旧 ORM 并存一段时间,确认稳定后再移除旧依赖。

十、常见陷阱与最佳实践

五大陷阱

  1. N+1 查询:循环中执行关联查询是性能头号杀手。Prisma 用 include/select 预加载关联,Drizzle 用 with,TypeORM 用 relations,Sequelize 用 include。务必养成"查列表时预加载关联"的习惯。
  2. 事务范围不当:长事务持有连接不放,导致连接池耗尽。Prisma 用 $transaction,Drizzle 用 db.transaction,事务应控制在 100ms 内完成。超时的事务不如不加。
  3. 忽略软删除的查询过滤:如果模型有 deletedAt 字段但查询忘加 where: { deletedAt: null },会查出已删除数据。建议在中间件层统一处理软删除过滤。
  4. 迁移直接在生产执行prisma migrate dev 会触发数据库 reset,绝不能在生产环境跑。生产用 migrate deploy。Drizzle 同理:push 仅限开发。
  5. 过度依赖 ORM 默认行为:比如 TypeORM 的 @OneToMany 默认不加载关联(lazy 模式),Sequelize 的 include 可能生成低效 JOIN。理解底层 SQL 行为,必要时用 $queryRaw 或原生驱动绕过 ORM。

最佳实践 Checklist

# Prisma 生产环境 Checklist
- [ ] 使用 prisma migrate deploy(而非 dev)部署迁移
- [ ] 配置 connection pool(DATABASE_URL 带 pool_timeout)
- [ ] 日志中间件记录慢查询(prisma.$extends)
- [ ] 开启 Prisma Accelerate 或 Data Proxy(Serverless 场景)
- [ ] CI 中运行 prisma validate 验证 schema

# Drizzle 生产环境 Checklist
- [ ] drizzle-kit generate 后手动检查迁移 SQL
- [ ] 自定义连接池配置(node-postgres Pool)
- [ ] 慢查询日志包裹层
- [ ] Edge Runtime 兼容性验证(Cloudflare Workers)
- [ ] CI 中运行 drizzle-kit check

# 通用 Checklist
- [ ] CI 中验证迁移可正确执行
- [ ] 定期审查迁移历史,清理冗余迁移
- [ ] 压测验证批量操作性能
- [ ] 监控连接池使用率和等待时间
- [ ] 限制单个查询返回行数(分页或 limit)

十一、总结

选 ORM 不是选"最好的",而是选"最适合你的场景的"。Prisma 是综合体验最好的选择,适合大多数中大型 TypeScript 项目,类型安全和开发效率无可挑剔;Drizzle 是高性能和 Edge 场景的利器,适合对性能和控制力有极高要求的团队,极薄抽象层让它接近原生 SQL 性能;TypeORM 在 NestJS 生态中仍然是主流选择,装饰器风格让 Java 背景开发者感到亲切;Sequelize 维护遗留项目可以,新项目不建议选。

最后记住一个原则:ORM 是工具不是信仰。当某个查询 ORM 写不好时,直接用 $queryRaw 或原生驱动写 SQL 完全没问题。Prisma 的 $queryRaw、Drizzle 的 sql 模板、TypeORM 的 query() 都支持原生 SQL 逃生通道。控制好 ORM 的使用边界,让它真正为你服务而不是给你添堵——这才是工程师该有的技术判断力。