Next.js: 数据库集成:Prisma
最后更新:2026-08-26
Prisma 是 Node.js 生态最流行的 ORM — 用 TypeScript 写数据模型,自动生成类型安全的数据库客户端。
1. 你将学到
- Prisma ORM 的安装、初始化与项目结构
- Schema 数据建模(User / Post / Comment 关联关系)
- 数据库迁移(
prisma migrate dev)与 Prisma Studio 可视化 - Server Component 中读取数据、Server Action 中写入数据
- Route Handler 中 CRUD 操作
- Prisma 连接池管理与 Next.js 兼容性
2. 一个全栈工程师的真实故事
(1) 痛点:纯 SQL 让开发效率暴跌
Bob 是 TaskFlow 团队的技术主管。团队用原生 SQL 操作 PostgreSQL:
"每个 API 端点要写 20 行 SQL 模板代码,
JOIN查询容易漏字段,改表结构要手动维护迁移脚本,最痛苦的是 TypeScript 类型和数据库字段不同步——运行时才发现列名写错。"
| 问题 | 每周耗时 | 影响 |
|---|---|---|
| 手写 SQL 模板 | 8h | 重复劳动 |
| 调试类型不匹配 | 4h | 运行时报错 |
| 手动迁移脚本 | 3h | 容易漏改 |
| 文档不同步 | 2h | 新人上手慢 |
(2) Prisma ORM 的解法
用 Prisma Schema 定义模型 → 自动生成 TypeScript 类型 → 类型安全的 CRUD。
PRISMA
// prisma/schema.prisma — 数据模型即文档
model User {
id String @id @default(cuid())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
}
(3) 收益
| 维度 | 手写 SQL | Prisma |
|---|---|---|
| 每 API CRUD 代码量 | 25 行 | 3 行 |
| 类型安全 | ❌ 手动定义 | ✅ 自动生成 |
| 迁移管理 | 手动 SQL 文件 | prisma migrate dev |
| 开发体验 | 切换 IDE/DB 客户端 | Prisma Studio 内嵌 |
| 文档同步 | 外挂 | Schema 即文档 |
3. Prisma 安装与项目初始化
(1) 安装流程
BASH
# 1. 安装 Prisma CLI 和客户端
npm install prisma @prisma/client --save-dev
# 或一次完成
npx prisma init --datasource-provider postgresql
graph LR
A[prisma init] --> B[生成 prisma/schema.prisma]
A --> C[生成 .env DATABASE_URL]
B --> D[定义数据模型]
D --> E[prisma migrate dev]
E --> F[生成 Prisma Client]
F --> G[导入使用]
style A fill:#cce5ff
style D fill:#d4edda
style F fill:#fff3cd
| 生成文件 | 作用 |
|---|---|
prisma/schema.prisma |
数据模型定义 |
.env |
数据库连接字符串(DATABASE_URL) |
(2) 初始化 Client 单例
TS
// lib/prisma.ts — 全局单例(防止热重载时创建多个连接)
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
💡 提示: Next.js 热重载会导致每次刷新创建新 PrismaClient 实例。全局单例模式在开发环境复用已有连接,避免"Too many connections"报错。
▶ 示例:验证数据库连接
TS
// app/api/health/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
try {
await prisma.$connect()
return NextResponse.json({ status: 'ok', db: 'connected' })
} catch (e) {
return NextResponse.json({ status: 'error', message: (e as Error).message }, { status: 500 })
}
}
💻 输出:
JSON
{ "status": "ok", "db": "connected" }
4. Schema 数据建模
(1) 模型关系类型
graph TB
User -->|一对多| Post
User -->|一对多| Comment
Post -->|一对多| Comment
Post -->|多对多| Tag
subgraph User
U1[id String @id @default(cuid())]
U2[name String]
U3[email String @unique]
end
subgraph Post
P1[id String @id @default(cuid())]
P2[title String]
P3[content String?]
end
subgraph Comment
C1[id String @id @default(cuid())]
C2[body String]
end
subgraph Tag
T1[id String @id @default(cuid())]
T2[name String @unique]
end
style User fill:#d4edda
style Post fill:#cce5ff
style Comment fill:#f8d7da
style Tag fill:#fff3cd
| 关系类型 | Prisma 语法 | 数据库实现 |
|---|---|---|
| 一对一 | User Profile |
外键 + unique |
| 一对多 | User Post[] |
外键 |
| 多对多 | Post Tag[] (隐式) |
中间表 _PostToTag |
| 自引用 | Category parentCategory |
自关联外键 |
(2) 完整 TaskFlow Schema
PRISMA
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
ADMIN
EDITOR
VIEWER
}
model User {
id String @id @default(cuid())
name String
email String @unique
role Role @default(VIEWER)
password String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
tags Tag[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}
model Comment {
id String @id @default(cuid())
body String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String
author String
createdAt DateTime @default(now())
@@index([postId])
}
model Tag {
id String @id @default(cuid())
name String @unique
posts Post[]
@@index([name])
}
▶ 示例:Prisma Studio 可视化操作
BASH
# 启动 Prisma Studio(浏览器 GUI 查看/编辑数据)
npx prisma studio
TEXT
📖 仅展示
1. 运行 `npx prisma studio`
2. 浏览器打开 http://localhost:5555
3. 左侧选择 User / Post / Comment / Tag 表
4. 点击 "Add Record" 添加测试数据
5. 点击 "Save Changes" 持久化
💡 提示: Studio 支持直接编辑关联数据,修改 User 时可以看到其 Posts 和 Comments。
5. 数据库迁移
(1) 迁移工作流
| 步骤 | 命令 | 作用 |
|---|---|---|
| 修改 Schema | 编辑 schema.prisma |
增减模型/字段 |
| 创建迁移 | npx prisma migrate dev --name add_user_role |
生成 SQL 迁移文件 |
| 应用迁移 | 自动执行 | 更新数据库结构 |
| 重置数据库 | npx prisma migrate reset |
清空数据 + 重新迁移 |
| 生成客户端 | npx prisma generate |
更新 TypeScript 类型 |
(2) 迁移文件结构
BASH
prisma/migrations/
├── 20260706000001_init/
│ └── migration.sql # 初始表创建
├── 20260706000002_add_user_role/
│ └── migration.sql # ALTER TABLE 添加 role 列
└── migration_lock.toml # 数据库提供商锁定
▶ 示例:Add Role 迁移
BASH
npx prisma migrate dev --name add_role_enum
生成的 SQL:
SQL
-- prisma/migrations/20260706000002_add_role_enum/migration.sql
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'EDITOR', 'VIEWER');
-- AlterTable
ALTER TABLE "User" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'VIEWER';
💡 提示:
prisma migrate dev 会自动检测 Schema 变化,生成对应的 SQL 语句。无需手动写 ALTER TABLE。
6. CRUD 操作实战
(1) Server Component 读取数据
TSX
// app/posts/page.tsx — RSC 直接查数据库
import { prisma } from '@/lib/prisma'
export default async function PostsPage() {
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: { select: { name: true } }, tags: true },
orderBy: { createdAt: 'desc' },
take: 20
})
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>作者: {post.author.name}</p>
<p>{post.tags.map((t) => t.name).join(', ')}</p>
</article>
))}
</div>
)
}
(2) Server Action 写入数据
TS
// app/actions/posts.ts — Server Action CRUD
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(data: { title: string; content?: string; authorId: string }) {
const post = await prisma.post.create({ data })
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}
export async function deletePost(id: string) {
await prisma.post.delete({ where: { id } })
revalidatePath('/posts')
}
▶ 示例:表单 + Server Action 创建文章
TSX
// app/posts/new/page.tsx
import { createPost } from '@/app/actions/posts'
import { auth } from '@/auth'
export default async function NewPostPage() {
const session = await auth()
if (!session?.user) return <p>请先登录</p>
return (
<form action={createPost}>
<input name="title" placeholder="文章标题" required />
<textarea name="content" placeholder="文章内容" rows={10} />
<input type="hidden" name="authorId" value={session.user.id} />
<button type="submit">发布</button>
</form>
)
}
TS
// 修正 Server Action:接收 FormData
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const authorId = formData.get('authorId') as string
if (!title || !authorId) throw new Error('缺少必要字段')
const post = await prisma.post.create({
data: { title, content, authorId }
})
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}
(3) Route Handler 完整 CRUD
TS
// app/api/posts/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await prisma.post.findMany({
include: { author: true, comments: true },
orderBy: { createdAt: 'desc' }
})
return NextResponse.json(posts)
}
export async function POST(req: Request) {
const body = await req.json()
const post = await prisma.post.create({ data: body })
return NextResponse.json(post, { status: 201 })
}
TS
// app/api/posts/[id]/route.ts
import { prisma } from '@/lib/prisma'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await prisma.post.findUnique({
where: { id },
include: { comments: true, tags: true }
})
if (!post) return NextResponse.json({ error: 'not found' }, { status: 404 })
return NextResponse.json(post)
}
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const body = await req.json()
const post = await prisma.post.update({ where: { id }, data: body })
return NextResponse.json(post)
}
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
await prisma.post.delete({ where: { id } })
return NextResponse.json({ deleted: true })
}
7. 连接池管理
(1) 连接池配置
| 配置项 | 默认值 | 生产建议 | 说明 |
|---|---|---|---|
connection_limit |
10 | 20~50 | 最大并发连接数 |
pool_timeout |
10s | 30s | 等待连接超时 |
idle_timeout |
10s | 30s | 空闲连接保留时间 |
(2) 配置连接池参数
TS
// 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', 'warn', 'error'] : ['error'],
datasources: {
db: {
url: process.env.DATABASE_URL + '?connection_limit=20&pool_timeout=30&idle_timeout=30'
}
}
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
⚠️ 注意: Serverless 环境中(Vercel),数据库连接数受限。建议使用 Prisma Accelerate(连接池代理)或 Supabase 连接池(
?pgbouncer=true)。
▶ 示例:Supabase 连接池配置
ENV
# .env — Supabase 连接池
DATABASE_URL="postgresql://postgres:password@db.xxxxx.supabase.co:6543/postgres?pgbouncer=true&connection_limit=5"
TS
// Serverless 优化:每次请求复用实例
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
export function getPrisma() {
if (!prisma) {
prisma = new PrismaClient({
datasources: { db: { url: process.env.DATABASE_URL } }
})
}
return prisma
}
8. 完整示例:博客系统 CRUD 综合实现
TSX
// app/posts/[id]/page.tsx — 文章详情 + 评论功能
import { prisma } from '@/lib/prisma'
import { notFound } from 'next/navigation'
import { auth } from '@/auth'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
async function addComment(formData: FormData) {
'use server'
const session = await auth()
if (!session?.user) throw new Error('请先登录')
const body = formData.get('body') as string
const postId = formData.get('postId') as string
if (!body || !postId) throw new Error('缺少字段')
await prisma.comment.create({
data: { body, postId, author: session.user.name! }
})
revalidatePath(`/posts/${postId}`)
}
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await prisma.post.findUnique({
where: { id },
include: {
author: { select: { name: true } },
tags: true,
comments: { orderBy: { createdAt: 'desc' } }
}
})
if (!post) notFound()
return (
<div>
<h1>{post.title}</h1>
<p>作者: {post.author.name}</p>
<div>{post.content}</div>
<p>标签: {post.tags.map((t) => t.name).join(', ')}</p>
<hr />
<h2>评论 ({post.comments.length})</h2>
{post.comments.map((c) => (
<div key={c.id}>
<strong>{c.author}</strong>: {c.body}
</div>
))}
<form action={addComment}>
<input type="hidden" name="postId" value={post.id} />
<textarea name="body" placeholder="写评论..." rows={3} required />
<button type="submit">提交</button>
</form>
</div>
)
}
💡 提示: 此示例在一个页面中同时演示了 RSC 数据读取(
prisma.post.findUnique)和 Server Action 写入(addComment),是 Next.js + Prisma 的全栈模式。
❓ 常见问题
Q Prisma 和 Drizzle ORM 应该选哪个?
A Prisma 对新手更友好(声明式 Schema + Studio 可视化 + 自动迁移),Drizzle 更接近 SQL 语法、性能略优。本教程使用 Prisma 因其生态最大(43k⭐)且文档完善。
Q 为什么要用全局单例模式创建 PrismaClient?
A Next.js 开发模式下热重载会频繁创建新实例,导致数据库连接数爆满。全局单例在
globalThis 上缓存实例,确保只创建一个 PrismaClient。Q
prisma migrate dev 和 prisma db push 有什么区别?A
migrate dev 生成可追溯的 SQL 迁移文件(适合团队协作),db push 直接同步 Schema 到数据库(适合快速原型,不保留历史)。生产环境必须用 migrate deploy。Q Server Component 中直接查数据库会不会有性能问题?
A 不会。RSC 运行在服务端,直连数据库查询比通过 API Route 少一次 HTTP 跳转。配合 Next.js 数据缓存(
fetch 自动缓存)效果更佳。Q 如何确保数据库操作的事务性?
A Prisma 支持嵌套写入(
create: { post: { create: {...} } })自动包裹事务。需要显式事务时用 prisma.$transaction([...]) 或 prisma.$transaction(async (tx) => {...})。📖 小节
- Prisma ORM 通过 Schema 声明式定义模型,自动生成类型安全的 TypeScript Client
- 三种关系模型:一对一、一对多(外键)、多对多(隐式中间表)
- 迁移工作流:修改 Schema →
migrate dev→ 自动生成 SQL → 更新 Client - Prisma Studio 提供浏览器 GUI 直接查看和编辑数据
- CRUD 操作可在 RSC(读取)、Server Action(写入)、Route Handler(API)中灵活使用
- 全局单例模式防止开发环境连接泄漏;Serverless 环境需加连接池参数
📝 作业
-
基础题(⭐):在现有项目中初始化 Prisma,定义 User 和 Profile 一对一模型,运行迁移后插入一条数据并在 Prisma Studio 中验证。
-
进阶题(⭐⭐):实现文章系统的完整 CRUD API Route — GET(列表+分页)、POST(创建)、PATCH(更新)、DELETE(删除),全部使用 Prisma 操作。
-
挑战题(⭐⭐⭐):使用 Prisma
$transaction实现一次创建文章同时关联标签的功能。如果标签不存在则先创建标签,然后建立文章与标签的多对多关联。