为什么选择 Prisma
JavaScript 世界的 ORM 不少——Sequelize(老牌)、TypeORM(装饰器风格)、Drizzle(轻量 SQL)……Prisma 的独特之处在于:
- 自动生成类型:从 Schema 文件自动生成完整的 TypeScript 类型,包括每个查询的返回类型
- 声明式 Schema:用简洁的 Prisma Schema 语言描述数据模型,不是用类装饰器
- 数据库迁移:
prisma migrate自动追踪 Schema 变更,生成 SQL 迁移文件 - Prisma Studio:内置的数据库 GUI,无需安装其他工具
安装与初始化
# 安装 Prisma CLI 和客户端
bun add prisma -d
bun add @prisma/client
# 初始化(生成 prisma/schema.prisma)
bunx prisma init --datasource-provider postgresql
# 或 sqlite / mysql / mongodb
# .env — 数据库连接
DATABASE_URL="postgresql://postgres:password@localhost:5432/mydb?schema=public"
# SQLite(开发/测试首选)
DATABASE_URL="file:./dev.db"
Prisma Schema 语法
Schema 文件(prisma/schema.prisma)是 Prisma 的核心,用 PSL(Prisma Schema Language)描述:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// 用户模型
model User {
id String @id @default(cuid())
email String @unique
name String?
role Role @default(USER)
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关系字段
posts Post[]
profile Profile?
@@index([email]) // 为邮箱建索引
@@map("users") // 映射到数据库表名
}
// 枚举
enum Role {
USER
ADMIN
MODERATOR
}
// 个人资料(一对一)
model Profile {
id String @id @default(cuid())
bio String?
avatar String?
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
// 文章(多对多标签)
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
authorId String
author User @relation(fields: [authorId], references: [id])
tags Tag[] @relation("PostToTag") // 多对多
createdAt DateTime @default(now())
@@index([authorId])
}
model Tag {
id String @id @default(cuid())
name String @unique
posts Post[] @relation("PostToTag")
}
数据库迁移
# 开发环境:创建并应用迁移
bunx prisma migrate dev --name add_user_table
# 这会:1. 生成 SQL 迁移文件 2. 应用迁移 3. 重新生成 Prisma Client
# 查看迁移历史
bunx prisma migrate status
# 生产环境:只应用已有迁移(不生成新迁移)
bunx prisma migrate deploy
# 重置数据库(危险!删除所有数据)
bunx prisma migrate reset
# 直接推送 Schema(不生成迁移文件,适合原型期)
bunx prisma db push
# 打开 Prisma Studio(可视化数据库管理)
bunx prisma studio
迁移文件要提交到 git:prisma/migrations/ 目录下的 SQL 文件是数据库历史的记录,必须提交。这样团队成员和 CI 都能重现相同的数据库状态。
Prisma Client — CRUD 操作
初始化客户端(单例模式)
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
// 开发环境防止热重载创建多个连接
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
基础 CRUD
import { prisma } from './lib/prisma';
// ─── CREATE ───
const user = await prisma.user.create({
data: {
email: 'zhang@example.com',
name: '张三',
password: hashedPassword,
profile: {
create: { bio: '全栈工程师' } // 同时创建关联数据
}
},
include: { profile: true }, // 返回包含 profile 的完整对象
});
// 批量创建
await prisma.user.createMany({
data: [
{ email: 'a@example.com', name: '用户A', password: 'hash1' },
{ email: 'b@example.com', name: '用户B', password: 'hash2' },
],
skipDuplicates: true,
});
// ─── READ ───
const user = await prisma.user.findUnique({
where: { email: 'zhang@example.com' },
select: { id: true, name: true, email: true }, // 只查询需要的字段
});
const users = await prisma.user.findMany({
where: {
role: 'USER',
createdAt: { gte: new Date('2024-01-01') },
OR: [
{ name: { contains: '张' } },
{ email: { endsWith: '@gmail.com' } },
],
},
orderBy: [{ createdAt: 'desc' }, { name: 'asc' }],
skip: 20, // 分页:跳过前20条
take: 10, // 每页10条
});
// ─── UPDATE ───
const updated = await prisma.user.update({
where: { id: 'user-id' },
data: { name: '新名字', updatedAt: new Date() },
});
// upsert = update or insert
const upserted = await prisma.user.upsert({
where: { email: 'zhang@example.com' },
update: { name: '更新名字' },
create: { email: 'zhang@example.com', name: '张三', password: 'hash' },
});
// ─── DELETE ───
await prisma.user.delete({ where: { id: 'user-id' } });
await prisma.user.deleteMany({ where: { role: 'USER', createdAt: { lt: cutoffDate } } });
关系查询
// 一对多:查询用户及其所有文章
const userWithPosts = await prisma.user.findUnique({
where: { id: 'user-id' },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 5,
select: { id: true, title: true, createdAt: true },
},
profile: true,
},
});
// 多对多:查询文章及其标签
const post = await prisma.post.findUnique({
where: { id: 'post-id' },
include: { tags: true, author: { select: { name: true, email: true } } },
});
// 连接多对多关系
await prisma.post.update({
where: { id: 'post-id' },
data: {
tags: {
connect: [{ id: 'tag-1' }, { id: 'tag-2' }],
disconnect: [{ id: 'tag-old' }],
}
}
});
// 聚合查询
const stats = await prisma.post.aggregate({
_count: { id: true },
where: { published: true },
});
// stats._count.id — 已发布文章总数
// groupBy
const postsByAuthor = await prisma.post.groupBy({
by: ['authorId'],
_count: { id: true },
where: { published: true },
orderBy: { _count: { id: 'desc' } },
});
事务
// 方式1:$transaction 数组(最简单)
const [newUser, newProfile] = await prisma.$transaction([
prisma.user.create({ data: { email: 'a@b.com', password: 'hash' } }),
prisma.profile.create({ data: { bio: 'Hello', userId: 'will-be-filled' } }),
]);
// 方式2:交互式事务(可包含条件逻辑)
const result = await prisma.$transaction(async (tx) => {
// 检查余额
const sender = await tx.account.findUnique({ where: { id: senderId } });
if (!sender || sender.balance < amount) {
throw new Error('余额不足'); // 自动回滚
}
// 扣款
await tx.account.update({
where: { id: senderId },
data: { balance: { decrement: amount } },
});
// 收款
await tx.account.update({
where: { id: receiverId },
data: { balance: { increment: amount } },
});
// 记录流水
return tx.transaction.create({
data: { senderId, receiverId, amount, type: 'TRANSFER' },
});
}, {
maxWait: 5000, // 等待连接最多 5 秒
timeout: 10000, // 事务超时 10 秒
});
Prisma Accelerate — 连接池与缓存
Serverless 和边缘环境的最大问题是数据库连接数爆炸(每个函数实例都创建新连接)。Prisma Accelerate 解决了这个问题:
- 全球连接池:数千个并发请求复用有限的数据库连接
- 边缘缓存:查询结果在边缘节点缓存,降低数据库压力
- 无冷启动连接开销:连接预热,消除 Serverless 冷启动时的连接延迟
// 安装 Accelerate 扩展
bun add @prisma/extension-accelerate
import { PrismaClient } from '@prisma/client/edge'; // 使用 Edge 版本
import { withAccelerate } from '@prisma/extension-accelerate';
const prisma = new PrismaClient().$extends(withAccelerate());
// 带缓存的查询
const users = await prisma.user.findMany({
cacheStrategy: {
ttl: 60, // 缓存 60 秒
swr: 600, // stale-while-revalidate:后台刷新时可使用旧缓存
},
});
Drizzle ORM 的崛起:如果你觉得 Prisma 太重(需要独立的守护进程、Schema 文件),可以考虑 Drizzle ORM——它用纯 TypeScript 定义表结构,无代码生成,bundle size 更小,更适合 Cloudflare Workers 等边缘环境。