← Back to Index

Chapter 7: Database Integration

Prisma ORM, Schema Definition & CRUD Operations

1. Prisma Installation & Initialization

When integrating a relational database into a Next.js project, Prisma provides a type-safe client and migration workflow. First install the CLI and runtime dependencies:

npm install prisma @prisma/client

Run initialization in the project root to create default configuration and example environment files:

npx prisma init

Common generated files:

Configure the connection string in .env, adjusting protocol and parameters for your chosen database:

# 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 supports PostgreSQL, MySQL, SQLite, SQL Server, CockroachDB, MongoDB, and more; specify the provider in schema.prisma's datasource.

💡 Key Point

Prisma is one of the most popular TypeScript ORMs in the Next.js ecosystem: Schema is the single source of types, with integrated migration and client generation that works smoothly with App Router and Server Actions.

2. Schema Definition

prisma/schema.prisma consists of three typical parts: datasource (which database to connect to), generator (how to generate Prisma Client), and several models (table structures and relationships).

Below is a complete example with User and Post in a one-to-many relationship (PostgreSQL). Prisma uses its own DSL; shown here with TypeScript-style highlighting as an approximation (install the Prisma editor plugin for accurate coloring):

// 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])
}

After running prisma migrate dev, the migration directory will contain SQL generated for the actual database (PostgreSQL example):

-- 示意:由 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");

Note: Post.authorId is the foreign key; User.posts and Post.author form bidirectional navigation. For many-to-many, add a Tag and PostTag join table with bidirectional @relation.

3. Migration & Database Sync

After modifying the Schema, you need to align the database structure and regenerate the type-safe client.

Migration (recommended for teams and traceable history)

npx prisma migrate dev --name init

This command generates SQL migration files based on the current schema.prisma, applies them to the development database, and runs prisma generate.

Quick Push (prototyping phase)

npx prisma db push

Syncs the Schema directly to the database without generating migration history; suitable for personal experiments or temporary environments. Production environments should prefer the migrate workflow.

Visualization & Client

npx prisma studio
npx prisma generate

Typical development flow: Modify Schema → migrate dev (or db push) → Use prisma client in code. After pulling others' migrations, run npx prisma migrate deploy (production) to sync the database.

4. Prisma Client Singleton

Problem: In Next.js development mode, modules may be re-executed multiple times due to hot reload. Creating a new PrismaClient each time can accumulate database connections.

Solution: Cache the singleton on the global object, creating it only when it doesn't exist. In production, also consider using connection pools (e.g., PgBouncer) and managed database limits.

Create 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;

Then uniformly use import { prisma } from "@/lib/prisma" in Server Components, Route Handlers, and Server Actions (path alias needs @/* configured in tsconfig.json).

5. CRUD Operations

The following assumes User / Post models are defined as above and the client has been generated.

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 },
});

Relation Queries: 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 } } },
});

Pagination: skip / take

const page = 2;
const pageSize = 10;

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

6. Combining with Server Actions

In App Router, you can call prisma directly inside server functions (with "use server") for form submissions and list refresh after changes. The example below includes: a create post form, a list queried by Server Component, deletion triggered by Server Action, and revalidatePath to invalidate the cached page.

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 (List + Form + Delete)

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>
  );
}

Note: createPost and deletePost execute only on the server; the page defaults to Server Component with data read server-side. After changes, revalidatePath("/posts") triggers re-rendering and data update for that route.

7. Environment Variables Configuration

Example (structural illustration only, replace with real passwords):

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

💡 Key Point

Never hardcode database passwords or API keys in source code; always inject through environment variables, and rotate them via hosting platforms and secret management tools.

8. Drizzle ORM Introduction

Drizzle is another popular TypeScript data layer solution, leaning toward "SQL-style" with a lightweight runtime, often paired with drizzle-kit for migrations.

Prisma vs Drizzle ORM

  • Prisma: Declarative Schema with integrated migration and Studio; higher-level API abstraction; rich ecosystem and tutorials.
  • Drizzle: Closer to SQL with chained/functional API; typically lighter bundle and runtime; advantages in performance-sensitive or strong SQL control scenarios.

Drizzle query example (conceptual demo):

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));
}

Recommendation: If your team is familiar with SQL and wants minimal abstraction with fine-grained control, evaluate Drizzle; for rapid modeling, an all-in-one toolchain, and abundant existing resources, Prisma remains a solid starting point. Both work with Next.js Server Components / Server Actions.

9. Chapter Summary

Installation & Configuration

prisma + @prisma/client; prisma init generates Schema and .env; DATABASE_URL points to target database.

Schema & Relations

Define Models, field types, and @relation in schema.prisma; model one-to-many / many-to-many per business needs.

Migration & Tools

migrate dev manages versions; db push for quick sync; studio manages data; generate updates the client.

Singleton & CRUD

src/lib/prisma.ts global singleton prevents dev-mode connection storms; master create / findMany / update / delete, include, skip+take.

Server Actions

Call Prisma inside "use server"; submit via form action; pair with revalidatePath to refresh lists.

Security & Alternatives

Secrets go in environment variables only; understand Drizzle and other alternatives; choose between Prisma and lightweight SQL-style ORMs based on team background.