Node.js 数据库集成:Prisma、TypeORM 与 Mongoose 实战

Node.js 数据库集成方案对比与实战:Prisma Schema 驱动开发、TypeORM 装饰器模式、Mongoose 文档模型,以及连接池、事务与性能优化。

1. ORM 选型对比

特性PrismaTypeORMMongooseDrizzle ORMSequelizeKnex.js
数据库PostgreSQL /MySQL /SQLite/MongoDB /SQL Server关系型为主MongoDB 专用PostgreSQL/MySQL/SQLitePostgreSQL/MySQL/SQLite/MSSQL关系型通用
类型安全⭐⭐⭐⭐⭐(自动生成类型)⭐⭐⭐⭐(装饰器)⭐⭐⭐(手动定义)⭐⭐⭐⭐⭐(类型安全 SQL)⭐⭐⭐(JS 为主)⭐⭐⭐⭐(部分类型)
查询 API链式、类型安全QueryBuilder/Repository链式查询SQL-like 类型构建器Model/QueryInterface原始查询构建器
连接池内置可配置内置可配置内置可配置依赖驱动内置可配置依赖驱动
事务支持交互式事务与批量事务QueryRunner 交互式事务MongoDB 事务事务包装器托管事务事务辅助
迁移内置 Migration CLITypeORM CLI不适用Drizzle KitSequelize CLIdb-migrate/knex migrate
性能中等(查询引擎 Rust 层)高高高(轻量运行时)中等高
学习曲线中等陡峭平缓低(熟悉 SQL 即可)中等低

选型建议:若团队追求极致类型安全且偏好声明式 Schema 驱动,Prisma 是首选;若需要灵活的装饰器模式与复杂 ORM 特性,TypeORM 更合适;MongoDB 场景毫无疑问选 Mongoose;若希望 SQL 完全透明且类型安全,Drizzle ORM 是当前社区增速最快的轻量方案;Sequelize 适合老项目维护;Knex.js 适合需要完全控制 SQL 的中台服务。


2. Prisma 实战

2.1 Schema 定义

Prisma 采用声明式 Schema 作为单一真相源,通过 prisma generate 自动推导 TypeScript 类型,编译时即可发现查询错误。

// schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  role      Role     @default(USER)
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  tags      Tag[]
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

enum Role {
  USER
  ADMIN
}

2.2 客户端使用

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
    log: ['query', 'info', 'warn', 'error'],
});

// 创建(自动类型校验)
const user = await prisma.user.create({
    data: { email: 'alice@example.com', name: 'Alice' }
});

// 查询 + 关联 + 分页
const posts = await prisma.post.findMany({
    where: { published: true },
    include: { author: true, tags: true },
    orderBy: { createdAt: 'desc' },
    take: 10,
    skip: 0
});

// 计数聚合
const count = await prisma.post.count({ where: { published: true } });

2.3 事务

Prisma 支持两种事务模式:批量事务数组与交互式事务。

// 数组模式:自动包裹为单一事务
const [user, post] = await prisma.$transaction([
    prisma.user.create({ data: { email: 'bob@example.com' } }),
    prisma.post.create({ data: { title: 'Hello', authorId: 1 } })
]);

// 交互式事务:支持读取-计算-写入
const result = await prisma.$transaction(async (tx) => {
    const user = await tx.user.findUnique({ where: { id: 1 } });
    if (!user) throw new Error('User not found');
    return tx.user.update({
        where: { id: 1 },
        data: { name: user.name + '_updated' }
    });
}, { isolationLevel: 'Serializable' }); // 可配置隔离级别

2.4 中间件

中间件允许在查询前后插入逻辑,如日志、权限校验、软删除、数据脱敏等。

prisma.$use(async (params, next) => {
    const before = Date.now();
    const result = await next(params);
    const after = Date.now();
    console.log(`Query ${params.model}.${params.action} took ${after - before}ms`);
    return result;
});

// 软删除中间件示例
prisma.$use(async (params, next) => {
    if (params.model === 'Post' && params.action === 'delete') {
        params.action = 'update';
        params.args.data = { deletedAt: new Date() };
    }
    return next(params);
});

2.5 Raw Query 与连接池配置

当 Prisma 提供的类型安全 API 无法满足复杂查询时,可直接执行原生 SQL 并映射为已知模型,兼顾灵活性与类型安全。

// 原生查询 + 映射
const users = await prisma.$queryRaw<User[]>`
    SELECT * FROM "User" WHERE email LIKE ${'%@example.com'}
`;

// 连接池高级配置
const prisma = new PrismaClient({
    datasources: {
        db: {
            url: process.env.DATABASE_URL
        }
    },
    connectionLimit: 20,           // 底层连接池大小(依赖 DATABASE_URL)
});

连接池 URL 参数示例:postgresql://user:pass@host/db?connection_limit=20&pool_timeout=10

2.6 Prisma Studio 高级用法

npx prisma studio --port 5555 --browser none   # 自定义端口
npx prisma migrate dev --name init             # 开发迁移
npx prisma migrate deploy                      # 部署迁移(生产环境)
npx prisma generate --watch                    # 监听 Schema 变更实时生成类型
npx prisma db seed                             # 执行 seed.ts 初始化测试数据

3. TypeORM 实战

TypeORM 采用装饰器驱动,高度贴合面向对象设计,适合 NestJS 等框架深度集成。

3.1 Entity 定义

import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

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

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

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

    @CreateDateColumn()
    createdAt: Date;

    @UpdateDateColumn()
    updatedAt: Date;

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

3.2 Repository 模式

const userRepo = dataSource.getRepository(User);

const user = await userRepo.findOne({
    where: { email: 'alice@example.com' },
    relations: ['posts'],
    cache: 60000          // 查询结果缓存 60 秒
});

// 条件构造
const users = await userRepo.find({
    where: { name: Like('%Alice%'), age: MoreThan(18) },
    order: { createdAt: 'DESC' },
    take: 10,
    skip: 0
});

3.3 原生 SQL 与 QueryBuilder

// 原生 SQL(注意 SQL 注入防护)
const users = await userRepo.query('SELECT * FROM users WHERE age > $1', [18]);

// QueryBuilder 链式复杂查询
const stats = await dataSource.getRepository(Post)
    .createQueryBuilder('post')
    .innerJoinAndSelect('post.author', 'author')
    .where('post.published = :published', { published: true })
    .andWhere('author.role = :role', { role: 'ADMIN' })
    .orderBy('post.createdAt', 'DESC')
    .getMany();

3.4 Migrations 与数据同步

TypeORM CLI 是管理数据库结构演进的核心工具。

npx typeorm migration:create -n AddUserRole        # 创建空迁移
npx typeorm migration:generate -n AutoMigration    # 基于 Entity 差异自动生成
npx typeorm migration:run                          # 执行未执行的迁移
npx typeorm migration:revert                       # 回滚最近一次迁移

迁移文件示例:

import { MigrationInterface, QueryRunner } from "typeorm";

export class AddUserRole implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.addColumn("user", new TableColumn({
            name: "role",
            type: "varchar",
            default: "'USER'"
        }));
    }
    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.dropColumn("user", "role");
    }
}

3.5 Subscriber 与事件监听

Subscriber 支持在插入、更新、删除前后触发逻辑,适合审计日志、缓存失效、索引同步。

import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm';

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
    listenTo() { return User; }

    beforeInsert(event: InsertEvent<User>) {
        console.log('Before insert:', event.entity);
    }

    afterUpdate(event: UpdateEvent<User>) {
        console.log('After update:', event.entity);
    }
}

3.6 Tree Entities 与 Caching

Tree Entities 支持邻接表、嵌套集、闭包表三种树形数据建模,是组织架构、分类目录等场景的标准解法。

import { Entity, Tree, TreeChildren, TreeParent, Column } from 'typeorm';

@Entity()
@Tree("closure-table")    // 可选:closure-table, nested-set, materialized-path
export class Category {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @TreeChildren()
    children: Category[];

    @TreeParent()
    parent: Category;
}

// 查询树
const treeRepo = dataSource.getTreeRepository(Category);
const roots = await treeRepo.findRoots();

Query Caching 利用 Redis 或内存缓存重复查询结果,配置 cache: true 或在 DataSource 中启用即可。


4. Mongoose 实战

4.1 Schema 定义

Mongoose Schema 通过预定义结构约束文档写入,同时保留 MongoDB 的灵活扩展能力。

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    email: { type: String, required: true, unique: true, index: true },
    name: String,
    age: { type: Number, min: 0 },
    tags: [String],
    metadata: {
        loginCount: { type: Number, default: 0 },
        lastLogin: Date,
        ipHistory: [{ ip: String, ts: Date }]
    }
}, { timestamps: true, collection: 'users' });

// 复合索引
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
userSchema.index({ 'metadata.lastLogin': -1 });

4.2 CRUD 与聚合

const User = mongoose.model('User', userSchema);

const user = await User.create({ email: 'test@example.com', name: 'Test' });

const users = await User.find({ age: { $gte: 18 } })
    .sort({ createdAt: -1 })
    .limit(10)
    .select('email name');

// 聚合管道:按标签分组统计
const stats = await User.aggregate([
    { $match: { age: { $exists: true } } },
    { $unwind: '$tags' },
    { $group: { _id: '$tags', count: { $sum: 1 }, avgAge: { $avg: '$age' } } },
    { $sort: { count: -1 } }
]);

4.3 Middleware 钩子

Mongoose 中间件(Pre/Post Hooks)在 init、validate、save、remove 等生命周期触发,可实现自动字段填充、数据校验、级联删除。

// 保存前自动更新摘要字段
userSchema.pre('save', function(next) {
    if (this.isModified('password')) {
        this.password = bcrypt.hashSync(this.password, 10);
    }
    next();
});

// 删除后清理关联评论
userSchema.post('findOneAndDelete', async function(doc) {
    if (doc) {
        await Comment.deleteMany({ author: doc._id });
    }
});

4.4 Virtuals 与 Populate 深度联查

Virtuals 是不存入数据库的计算属性,Populate 则是 Mongoose 解决文档数据库关联查询的核心机制。

// Virtual:全名拼接,不占用存储
userSchema.virtual('fullName').get(function() {
    return `${this.firstName} ${this.lastName}`;
});

// 支持 Populate 的引用字段
const postSchema = new mongoose.Schema({
    title: String,
    author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
    coAuthors: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
    comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }]
});

// 深度联查:多级 Populate + 字段筛选
const posts = await Post.find()
    .populate('author', 'name email')
    .populate({
        path: 'comments',
        populate: { path: 'author', select: 'name' },
        options: { limit: 5, sort: { createdAt: -1 } }
    });

4.5 Discriminator 多态模型

Discriminator 允许在同一集合中存储具有不同结构但共享基类行为的文档,是面向对象多态在 MongoDB 中的实践。

const eventSchema = new mongoose.Schema({
    name: String,
    createdAt: { type: Date, default: Date.now }
}, { discriminatorKey: 'eventType' });

const Event = mongoose.model('Event', eventSchema);

const ClickedLinkEvent = Event.discriminator('ClickedLink', new mongoose.Schema({
    url: { type: String, required: true }
}));

const SignedUpEvent = Event.discriminator('SignedUp', new mongoose.Schema({
    email: { type: String, required: true }
}));

await ClickedLinkEvent.create({ name: 'cta-click', url: 'https://example.com' });

5. Drizzle ORM:类型安全 SQL 构建器

Drizzle ORM 的设计理念是 “If you know SQL, you know Drizzle”。它不提供厚重的抽象层,而是将 SQL 语法映射为类型安全的 TypeScript 函数调用,运行时开销极低,Bundle 体积远小于 Prisma。

5.1 Schema 定义与类型推导

import { pgTable, serial, varchar, integer, timestamp, text } 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: 255 }),
    createdAt: timestamp('created_at').defaultNow(),
});

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

5.2 类型安全查询

import { eq, desc, sql } from 'drizzle-orm';
import { users, posts } from './schema';

// 单表查询
const allUsers = await db.select().from(users).where(eq(users.role, 'ADMIN'));

// 关联查询
const result = await db.select({
    postTitle: posts.title,
    authorName: users.name
}).from(posts)
    .innerJoin(users, eq(posts.authorId, users.id))
    .where(sql`${posts.createdAt} > now() - interval '7 days'`)
    .orderBy(desc(posts.createdAt))
    .limit(10);

5.3 Drizzle 与 Prisma 对比

维度PrismaDrizzle ORM
API 风格链式对象查询SQL 风格函数调用
运行时大小较大(含 Rust 查询引擎)极小(纯 JS/TS 驱动层)
类型生成prisma generate 主动生成从 TS Schema 直接推导
迁移工具Prisma MigrateDrizzle Kit
原生 SQL$queryRaw 返回未知类型sql 模板可推导列类型
适用场景快速开发、大团队协作性能敏感、微服务、边缘计算

6. 连接池管理与数据库性能优化

Node.js 数据库驱动普遍基于连接池复用 TCP 连接,避免每次请求创建新连接的昂贵开销。连接池过小会导致请求排队,过大则浪费内存并压垮数据库。

6.1 各 ORM 连接池配置

Prisma 通过连接字符串参数控制:

DATABASE_URL="postgresql://user:pass@localhost:5432/db?connection_limit=20&pool_timeout=10"

TypeORM 在 DataSource 中显式配置:

const dataSource = new DataSource({
    type: 'postgres',
    url: process.env.DATABASE_URL,
    extra: {
        max: 20,           // 最大连接数
        idleTimeoutMillis: 30000,
        connectionTimeoutMillis: 2000,
    }
});

Mongoose 连接选项:

await mongoose.connect(uri, {
    maxPoolSize: 20,
    minPoolSize: 5,
    serverSelectionTimeoutMS: 5000,
    socketTimeoutMS: 45000,
});

6.2 查询优化原则

  • 索引优先:在 WHERE、ORDER BY、JOIN 字段上建索引,定期使用 EXPLAIN 分析执行计划。
  • 避免 N+1:使用 include/populate/join 一次性获取关联数据,或在 Prisma 中开启批量查询优化。
  • 分页深翻优化:大数据量下避免 OFFSET 深分页,改用游标分页(Cursor-based Pagination)。
  • 结果集裁剪:始终指定 select 或投影字段,减少网络传输与序列化开销。

7. 多数据库与读写分离架构

生产环境常见的多数据库场景包括:业务主库 + 报表只读库、按租户分库、以及主从读写分离。

7.1 TypeORM 多数据源切换

export const readDataSource = new DataSource({
    type: 'postgres',
    url: process.env.DATABASE_READ_URL,
    entities: [User, Post],
    synchronize: false,
});

// 写操作走主库
const user = await writeRepo.save(newUser);
// 统计查询走只读库
const stats = await readRepo.query('SELECT COUNT(*) FROM users');

7.2 Prisma 读写分离扩展方案

Prisma 本身不原生支持多连接端点切换,但可以包装多个 Client 实例,或使用社区中间件实现在查询级别自动路由。

const prismaWrite = new PrismaClient({ datasources: { db: { url: PRIMARY_URL } } });
const prismaRead  = new PrismaClient({ datasources: { db: { url: REPLICA_URL } } });

// 读取代理函数
async function findUsers(args: Prisma.UserFindManyArgs) {
    return prismaRead.user.findMany(args);
}

7.3 Mongoose 多连接管理

const connAlpha = mongoose.createConnection(uriAlpha);
const connBeta  = mongoose.createConnection(uriBeta);

const UserAlpha = connAlpha.model('User', userSchema);
const UserBeta  = connBeta.model('User', userSchema);

8. ORM 性能对比基准测试

为量化不同 ORM 在实际查询中的延迟差异,以下给出基于 autocannon 与真实数据库的测试思路与典型结果参考。测试环境:Node.js 20、PostgreSQL 15、本地 Docker、100 并发、持续 10 秒。

8.1 测试方法

// benchmark.ts:测试简单 SELECT 与 JOIN 查询
import { PrismaClient } from '@prisma/client';
import { DataSource } from 'typeorm';
import { drizzle } from 'drizzle-orm/node-postgres';

async function benchmark() {
    // 各 ORM 执行相同语义的查询
    const prismaUser = await prisma.user.findUnique({ where: { id: 1 } });
    const typeormUser = await typeormRepo.findOne({ where: { id: 1 } });
    const drizzleUser = await db.select().from(users).where(eq(users.id, 1));
}

8.2 典型延迟参考

场景PrismaTypeORMDrizzle ORM
简单主键查询(FindByPk)1.8ms1.2ms0.9ms
单表分页查询(Limit 20)3.5ms2.8ms2.1ms
关联查询(JOIN + 10 条)5.2ms4.1ms3.0ms
原生 SQL(同语义对照)1.5ms1.0ms0.8ms

8.3 结果解读

Prisma 因 Rust 查询引擎的序列化与进程通信存在额外开销,在简单查询下略逊于 TypeORM 与 Drizzle;但在复杂关联与类型校验场景下优势显著。Drizzle 因几乎零抽象层,延迟最接近原生驱动,适合延迟极度敏感的服务。TypeORM 表现均衡,功能最为全面。选型时应以团队熟悉度、维护成本与业务复杂度为核心,而非单纯追求微秒级差异。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nodejs」更多文章

  1. Node.js 优雅停机与健康检查实战
  2. Node.js 内存泄漏诊断实战
  3. BullMQ 后台任务队列实战