6.1 什么是 Prisma ORM
Prisma 是 Node.js 和 TypeScript 生态中最流行的 ORM(对象关系映射)工具。它通过一套声明式的 Schema 语言定义数据模型,自动生成类型安全的查询客户端,让数据库操作如同操作 TypeScript 对象一样直观、安全。
Prisma 的核心优势在于类型安全:查询结果的类型会根据你的 Schema 自动推导,IDE 可以对所有数据库操作进行类型检查,彻底消除了因字段名拼写错误或类型不匹配导致的运行时错误。
-
Prisma Schema
使用
.prisma文件定义数据模型、关系、索引。这是 Prisma 的"单一数据源",数据库迁移和客户端类型都从 Schema 生成。 - Prisma Client 根据 Schema 自动生成的类型安全数据库客户端。支持 PostgreSQL、MySQL、SQLite、MongoDB、CockroachDB 等。
- Prisma Migrate 基于 Schema 变更自动生成 SQL 迁移文件,并跟踪迁移历史,确保数据库与 Schema 始终同步。
- Prisma Studio 内置的可视化数据库 GUI 工具,可以在浏览器中浏览、编辑数据,开发调试利器。
- Prisma Accelerate Prisma 提供的全球边缘连接池和缓存服务,解决 Serverless/Edge 环境的数据库连接问题。
6.2 安装与初始化
# 安装 Prisma 开发依赖和客户端
pnpm add -D prisma
pnpm add @prisma/client
# 初始化 Prisma(选择数据库)
pnpm prisma init --datasource-provider postgresql
# 或 sqlite(本地开发更简单)
pnpm prisma init --datasource-provider sqlite
SHELL
初始化后会生成 prisma/schema.prisma 文件和 .env 文件。在 .env 中配置数据库连接:
# .env
# PostgreSQL(使用 Vercel Postgres 或 Supabase)
DATABASE_URL="postgresql://user:password@host:5432/mydb?sslmode=require"
# SQLite(本地开发)
# DATABASE_URL="file:./dev.db"
ENV
6.3 Schema 定义
Prisma Schema 使用简洁的 DSL(领域特定语言)定义数据模型。每个 model 对应数据库中的一张表,字段类型会自动映射到对应数据库的列类型,并生成相应的 TypeScript 类型。
// 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?
image String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关系:一个用户有多篇文章
posts Post[]
comments Comment[]
}
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关系:文章属于某个作者(外键)
authorId String
author User @relation(fields: [authorId], references: [id])
// 关系:文章有多条评论
comments Comment[]
// 关系:文章属于多个标签(多对多)
tags Tag[]
@@index([authorId]) // 添加索引
@@map("posts") // 映射到数据库表名
}
model Comment {
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
authorId String
author User @relation(fields: [authorId], references: [id])
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
}
model Tag {
id String @id @default(cuid())
name String @unique
posts Post[]
}
enum Role {
USER
ADMIN
}
PRISMA
6.4 Prisma Migrate 数据库迁移
# 创建并应用迁移(开发环境)
pnpm prisma migrate dev --name init
# 生成 Prisma Client(每次修改 schema 后需要运行)
pnpm prisma generate
# 将迁移应用到生产数据库(不创建新迁移)
pnpm prisma migrate deploy
# 重置数据库(删除所有数据并重新迁移,仅用于开发!)
pnpm prisma migrate reset
# 打开可视化 Studio
pnpm prisma studio
SHELL
生产环境注意永远不要在生产环境运行 prisma migrate dev 或 prisma migrate reset,这两个命令可能删除数据。CI/CD 流程中应使用 prisma migrate deploy,它只会应用未执行的迁移,不会修改已有数据。
6.5 配置单例 Prisma Client
在 Next.js 开发模式下,每次热更新都会重新执行模块,如果不做特殊处理,会创建多个 PrismaClient 实例,导致"Too many connections"错误。标准做法是使用全局变量缓存实例。
// lib/db.ts — 单例模式
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const db = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db
}
TS
6.6 CRUD 操作与关系查询
Prisma Client 提供了直观的 API 进行数据操作。以下是常用 CRUD 操作的示例:
import { db } from '@/lib/db'
// ── CREATE ──
const post = await db.post.create({
data: {
title: 'Hello Next.js',
slug: 'hello-nextjs',
content: '...',
author: { connect: { id: userId } }, // 关联已有用户
tags: {
connectOrCreate: [ // 连接或创建标签
{ where: { name: 'Next.js' }, create: { name: 'Next.js' } }
]
}
},
include: { author: true, tags: true } // 同时返回关联数据
})
// ── READ(带过滤、排序、分页)──
const posts = await db.post.findMany({
where: {
published: true,
author: { role: 'ADMIN' }, // 关系过滤
title: { contains: 'Next' }, // 模糊搜索
},
orderBy: { createdAt: 'desc' },
skip: 0, // 分页:跳过 N 条
take: 10, // 分页:取 N 条
select: { // 只选择需要的字段
id: true, title: true, slug: true,
author: { select: { name: true, image: true } }
}
})
// ── UPDATE ──
await db.post.update({
where: { id: postId },
data: { viewCount: { increment: 1 } } // 原子递增
})
// ── DELETE(级联删除)──
await db.post.delete({ where: { id: postId } })
// Comment 因设置了 onDelete: Cascade 会自动删除
// ── 事务 ──
await db.$transaction([
db.post.update({ where: { id }, data: { published: true } }),
db.user.update({ where: { id: authorId }, data: { postCount: { increment: 1 } } }),
])
TS
select vs includeselect 只返回指定字段,include 在默认字段基础上加载关联数据。优先使用 select 仅获取页面需要的字段,避免过度获取(Over-fetching),尤其是不要在列表查询中加载大型文本字段(如 content)。