Next.js: 数据库集成:Prisma

最后更新:2026-08-26

Prisma 是 Node.js 生态最流行的 ORM — 用 TypeScript 写数据模型,自动生成类型安全的数据库客户端。

1. 你将学到


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
100%
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) 模型关系类型

100%
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 devprisma 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) => {...})

📖 小节


📝 作业

  1. 基础题(⭐):在现有项目中初始化 Prisma,定义 User 和 Profile 一对一模型,运行迁移后插入一条数据并在 Prisma Studio 中验证。

  2. 进阶题(⭐⭐):实现文章系统的完整 CRUD API Route — GET(列表+分页)、POST(创建)、PATCH(更新)、DELETE(删除),全部使用 Prisma 操作。

  3. 挑战题(⭐⭐⭐):使用 Prisma $transaction 实现一次创建文章同时关联标签的功能。如果标签不存在则先创建标签,然后建立文章与标签的多对多关联。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏