← 返回目录

第七章:数据库集成

Prisma ORM、Schema 定义与 CRUD 操作

1. Prisma 安装与初始化

在 Next.js 项目中集成关系型数据库时,Prisma 提供类型安全的客户端与迁移工作流。先安装 CLI 与运行时依赖:

npm install prisma @prisma/client

在项目根目录执行初始化,会创建默认配置与示例环境文件:

npx prisma init

常见生成文件:

.env 中配置连接串,按所选数据库调整协议与参数:

# PostgreSQL 示例
DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/mydb?schema=public"

# MySQL 示例
# DATABASE_URL="mysql://USER:PASSWORD@localhost:3306/mydb"

# SQLite(本地文件,适合学习与原型)
# DATABASE_URL="file:./dev.db"

Prisma 支持 PostgreSQLMySQLSQLiteSQL ServerCockroachDBMongoDB 等;在 schema.prismadatasource 中指定 provider 即可。

💡 要点

Prisma 是 Next.js 生态中最流行的 TypeScript ORM 之一:Schema 即类型来源,迁移与客户端生成一体化,与 App Router、Server Actions 配合顺畅。

2. Schema 定义

prisma/schema.prisma 由三部分典型内容组成:datasource(连哪个数据库)、generator(如何生成 Prisma Client)、以及若干 model(表结构与关系)。

下面是一份包含 UserPost、一对多关系的完整示例(PostgreSQL)。Prisma 使用自有 DSL,以下用 TypeScript 风格高亮近似展示(亦可安装编辑器 Prisma 插件获得准确着色):

// 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      String   @default("USER")
  profile   Json?
  createdAt DateTime @default(now())
  posts     Post[]
}

model Post {
  id        String   @id @default(cuid())
  title     String
  published Boolean  @default(false)
  content   String?
  authorId  String
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([authorId])
}

执行 prisma migrate dev 后,迁移目录中会生成面向真实数据库的 SQL(PostgreSQL 示意):

-- 示意:由 Prisma 生成的迁移文件中的 SQL 片段

CREATE TABLE "User" (
  "id" TEXT NOT NULL,
  "email" TEXT NOT NULL,
  "name" TEXT,
  CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

说明:Post.authorId 为外键;User.postsPost.author 构成双向导航。多对多可再增加 TagPostTag 中间表并双向 @relation

3. Migration 与数据库同步

修改 Schema 后,需要让数据库结构与之对齐,并重新生成类型安全的客户端。

迁移(推荐用于团队与可追溯历史)

npx prisma migrate dev --name init

该命令会基于当前 schema.prisma 生成 SQL 迁移文件、应用到开发数据库,并执行 prisma generate

快速推送(原型阶段)

npx prisma db push

不生成迁移历史,直接将 Schema 同步到数据库,适合个人实验或临时环境;生产环境更推荐 migrate 工作流。

可视化与客户端

npx prisma studio
npx prisma generate

典型开发流程:改 Schema → migrate dev(或 db push)→ 在代码中使用 prisma 客户端。拉取他人迁移后执行 npx prisma migrate deploy(生产)同步数据库。

4. Prisma Client 单例

问题:Next.js 开发模式下模块会随热重载多次执行,若每次新建 PrismaClient,可能堆积大量数据库连接。

解决:在全局对象上缓存单例,仅在不存在时创建;生产环境仍建议配合连接池(如 PgBouncer)与托管数据库限制。

创建 src/lib/prisma.ts

import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };

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;

之后在 Server Components、Route Handlers、Server Actions 中统一 import { prisma } from "@/lib/prisma"(路径别名需在 tsconfig.json 中配置 @/*)。

5. CRUD 操作

以下假设已按上文定义 User / Post 模型并生成客户端。

创建(Create)

import { prisma } from "@/lib/prisma";

await prisma.user.create({
  data: {
    email: "dev@example.com",
    name: "开发者",
  },
});

await prisma.post.create({
  data: {
    title: "第一篇",
    authorId: userId,
  },
});

查询(Read)

const users = await prisma.user.findMany({
  where: { role: "USER" },
  orderBy: { createdAt: "desc" },
});

const one = await prisma.user.findUnique({
  where: { email: "dev@example.com" },
});

const first = await prisma.post.findFirst({
  where: { published: true },
});

更新(Update)

await prisma.user.update({
  where: { id: userId },
  data: { name: "新名字" },
});

await prisma.post.updateMany({
  where: { authorId: userId },
  data: { published: true },
});

删除(Delete)

await prisma.post.delete({
  where: { id: postId },
});

关联查询:include / select

const postsWithAuthor = await prisma.post.findMany({
  include: { author: true },
});

const slim = await prisma.user.findMany({
  select: { id: true, email: true, _count: { select: { posts: true } } },
});

分页:skip / take

const page = 2;
const pageSize = 10;

const pageRows = await prisma.post.findMany({
  skip: (page - 1) * pageSize,
  take: pageSize,
  orderBy: { createdAt: "desc" },
});

6. 结合 Server Actions

在 App Router 中,可在服务器函数(带 "use server")内直接调用 prisma,实现表单提交与变更后列表刷新。下面示例包含:创建帖子表单、列表由 Server Component 查询、删除由 Server Action 触发,并用 revalidatePath 使缓存页面失效。

app/posts/actions.ts

"use server";

import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/prisma";

export async function createPost(formData: FormData) {
  const title = String(formData.get("title") ?? "").trim();
  const authorId = String(formData.get("authorId") ?? "").trim();
  if (!title || !authorId) return;

  await prisma.post.create({
    data: { title, authorId, published: false },
  });
  revalidatePath("/posts");
}

export async function deletePost(postId: string) {
  await prisma.post.delete({ where: { id: postId } });
  revalidatePath("/posts");
}

app/posts/page.tsx(列表 + 表单 + 删除)

import { prisma } from "@/lib/prisma";
import { createPost, deletePost } from "./actions";

export default async function PostsPage() {
  const posts = await prisma.post.findMany({
    orderBy: { createdAt: "desc" },
    include: { author: { select: { name: true, email: true } } },
  });

  const users = await prisma.user.findMany({ select: { id: true, email: true } });

  return (
    <div className="mx-auto max-w-2xl space-y-8 p-6">
      <form action={createPost} className="space-y-3 rounded border p-4">
        <input name="title" placeholder="标题" className="w-full border px-2 py-1" required />
        <select name="authorId" className="w-full border px-2 py-1" required>
          {users.map((u) => (
            <option key={u.id} value={u.id}>
              {u.email}
            </option>
          ))}
        </select>
        <button type="submit" className="rounded bg-black px-3 py-1 text-white">
          创建帖子
        </button>
      </form>

      <ul className="space-y-2">
        {posts.map((p) => (
          <li key={p.id} className="flex items-center justify-between border-b py-2">
            <div>
              <div className="font-medium">{p.title}</div>
              <div className="text-sm text-gray-500">
                {p.author.email} · {p.createdAt.toISOString()}
              </div>
            </div>
            <form action={deletePost.bind(null, p.id)}>
              <button type="submit" className="text-sm text-red-600">
                删除
              </button>
            </form>
          </li>
        ))}
      </ul>
    </div>
  );
}

说明:createPostdeletePost 仅在服务器执行;页面默认为 Server Component,数据在服务端读取。变更后 revalidatePath("/posts") 会触发该路径的重新渲染与数据更新。

7. 环境变量配置

示例(仅结构示意,真实密码请替换):

{
  "NOTE": "实际使用 .env 文件,每行 KEY=VALUE;此处为说明用 JSON 结构",
  "DATABASE_URL": "postgresql://user:password@host:5432/dbname"
}

💡 要点

永远不要将数据库密码、API 密钥硬编码在源码中;一律通过环境变量注入,并在托管平台与密钥管理工具中轮换。

8. Drizzle ORM 简介

Drizzle 是另一款流行的 TypeScript 数据层方案,偏「SQL 风格」与轻量运行时,常与 drizzle-kit 迁移工具配合使用。

Prisma vs Drizzle ORM

  • Prisma:声明式 Schema、迁移与 Studio 一体;API 偏高层封装;生态与教程丰富。
  • Drizzle:更贴近 SQL 的链式/函数 API;包体积与运行时通常更轻;性能敏感或强 SQL 控制场景有优势。

Drizzle 查询示例(概念演示):

import { eq } from "drizzle-orm";
import { db } from "./db";
import { posts } from "./schema";

export async function listPublished() {
  return db.select().from(posts).where(eq(posts.published, true));
}

选择建议:团队熟悉 SQL、希望最小抽象与细粒度控制时可评估 Drizzle;追求快速建模、一体化工具链与大量现成资料时,Prisma 仍是稳妥起点。二者都可与 Next.js Server Components / Server Actions 配合。

9. 本章要点

安装与配置

prisma + @prisma/clientprisma init 生成 Schema 与 .envDATABASE_URL 指向目标数据库。

Schema 与关系

schema.prisma 中定义 Model、字段类型与 @relation;一对多 / 多对多按业务建模。

迁移与工具

migrate dev 管理版本;db push 快速同步;studio 管理数据;generate 更新客户端。

单例与 CRUD

src/lib/prisma.ts 全局单例避免开发环境连接风暴;掌握 create / findMany / update / deleteincludeskip+take

Server Actions

"use server" 中调用 Prisma,表单 action 提交;配合 revalidatePath 刷新列表。

安全与选型

密钥只放环境变量;了解 Drizzle 等替代方案,按团队背景在 Prisma 与轻量 SQL 风格 ORM 间取舍。