首页 / Next.js 16 入门教程 / 数据库集成

Next.js 16 入门教程

数据库集成

本教程共 42 篇 · 第 26 篇 · 更新于 2026-07-30 · 约 7 分钟阅读

Next.jsNext.js 16 入门教程数据库PrismaDrizzleServer Components

26. 数据库集成

本节目标:理解 Next.js 中数据库集成的核心模式,掌握 Server Components 直连数据库的方式,了解 Prisma 和 Drizzle 两大 ORM 的特点与使用,以及连接池的最佳实践。

Server Components 直连数据库

App Router 有个好处:Server Components 可以直接访问数据库,不用再走 API 层中转。这样代码少了,也不会暴露内部数据源。

// app/posts/page.tsx
import { db } from '@/lib/db'

export default async function PostsPage() {
  // 直接在 Server Component 中查询数据库
  const posts = await db.query.posts.findMany({
    orderBy: desc(posts.createdAt),
    limit: 10,
  })

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}
Important

在 Server Components 中直接 fetch 数据时,不要使用 Route Handlers 中转。因为 Server Components 在服务器上执行,额外的 HTTP 请求只会增加延迟。

何时使用 Route Handlers

虽然 Server Components 可以直接访问数据库,但以下场景仍需 Route Handlers:

  1. 客户端组件需要数据:Client Components 无法直接访问数据库,需要通过 Route Handlers 获取数据
  2. 频繁轮询的数据:客户端需要实时更新的数据
  3. 依赖客户端 Web API 的数据:如地理位置、文件 API 等
// app/api/posts/route.ts
import { db } from '@/lib/db'

export async function GET() {
  const posts = await db.query.posts.findMany({
    orderBy: desc(posts.createdAt),
    limit: 10,
  })

  return Response.json(posts)
}

Prisma ORM

Prisma 是目前最流行的 Node.js ORM 之一,提供类型安全的查询 API 和自动迁移功能。

安装与初始化

npm install prisma @prisma/client
npx prisma init

定义数据模型

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

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

生成客户端并查询

npx prisma generate
npx prisma db push
// lib/db.ts
import { PrismaClient } from '@prisma/client'

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

export const db = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
// app/posts/page.tsx
import { db } from '@/lib/db'

export default async function PostsPage() {
  const posts = await db.post.findMany({
    include: { author: true },
    orderBy: { createdAt: 'desc' },
  })

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>作者: {post.author?.name}</p>
        </article>
      ))}
    </div>
  )
}

Drizzle ORM

Drizzle 是一个轻量级、类型安全的 ORM,更接近 SQL 语法,性能优秀。

安装与配置

npm install drizzle-orm pg
npm install -D drizzle-kit

定义 Schema

// db/schema.ts
import { pgTable, serial, text, boolean, timestamp, integer } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: text('email').notNull().unique(),
  name: text('name'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content'),
  published: boolean('published').default(false).notNull(),
  authorId: integer('author_id').references(() => users.id),
  createdAt: timestamp('created_at').defaultNow().defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

创建数据库连接

// db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
})

export const db = drizzle(pool, { schema })

查询数据

// app/posts/page.tsx
import { db } from '@/db'
import { posts, users } from '@/db/schema'
import { desc, eq } from 'drizzle-orm'

export default async function PostsPage() {
  const allPosts = await db
    .select({
      id: posts.id,
      title: posts.title,
      authorName: users.name,
    })
    .from(posts)
    .leftJoin(users, eq(posts.authorId, users.id))
    .orderBy(desc(posts.createdAt))

  return (
    <div>
      {allPosts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>作者: {post.authorName}</p>
        </article>
      ))}
    </div>
  )
}

Prisma vs Drizzle 对比

特性PrismaDrizzle
类型安全自动生成类型推断类型,无需生成
查询风格链式 API接近 SQL 的查询构建器
迁移工具内置迁移系统Drizzle Kit 迁移
性能良好更优(轻量级)
学习曲线中等较低(熟悉 SQL 的话)
包体积较大较小
关系查询直观需要手动 join

连接池配置

在 Serverless 环境(如 Vercel)中,数据库连接池非常重要。因为每个 Serverless 函数实例都会创建新的数据库连接,容易导致连接耗尽。

使用 Vercel Postgres 连接池

// lib/db.ts
import { db } from '@vercel/postgres'

export async function getData() {
  const client = await db.connect()
  try {
    const data = await client.sql`SELECT * FROM posts`
    return data.rows
  } finally {
    client.release()
  }
}

Prisma 连接池配置

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  directUrl = env("DIRECT_URL") // 用于迁移
}
// lib/db.ts
import { PrismaClient } from '@prisma/client'

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

export const db = globalForPrisma.prisma ?? new PrismaClient({
  datasourceUrl: process.env.DATABASE_URL,
})

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db

Drizzle 连接池配置

// db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'

// 全局缓存连接池
const globalForDb = globalThis as unknown as {
  pool: Pool | undefined
}

const pool = globalForDb.pool ?? new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // 最大连接数
})

if (process.env.NODE_ENV !== 'production') globalForDb.pool = pool

export const db = drizzle(pool)
Tip

在开发环境中,使用 globalThis 缓存数据库连接,避免热重载时创建多个连接。

数据访问层设计

推荐创建数据访问层(DAL)来集中管理数据库操作:

// lib/dal/posts.ts
import 'server-only'
import { db } from '@/lib/db'
import { posts } from '@/db/schema'
import { desc, eq } from 'drizzle-orm'
import { cache } from 'react'

// 使用 React cache 避免重复查询
export const getPosts = cache(async () => {
  return db.query.posts.findMany({
    orderBy: desc(posts.createdAt),
    limit: 10,
  })
})

export const getPostBySlug = cache(async (slug: string) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.slug, slug),
  })
  return post
})

export const getUserPosts = cache(async (userId: number) => {
  return db.query.posts.findMany({
    where: eq(posts.authorId, userId),
    orderBy: desc(posts.createdAt),
  })
})

数据变更操作

使用 Server Action 处理数据变更:

// app/actions/posts.ts
'use server'

import { db } from '@/lib/db'
import { posts } from '@/db/schema'
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

  // 验证数据
  if (!title || title.length < 3) {
    return { error: '标题至少需要 3 个字符' }
  }

  // 插入数据
  await db.insert(posts).values({
    title,
    content,
    authorId: 1, // 从会话中获取
  })

  // 清除缓存
  revalidatePath('/posts')

  // 重定向
  redirect('/posts')
}

小结

这一章我们学了数据库集成:

  1. Server Components 能直接查数据库,不用 Route Handlers 中转
  2. globalThis 缓存连接,避免开发环境连接泄漏
  3. Serverless 环境要注意连接池配置
  4. 用 DAL 集中管理数据库操作
  5. 数据变更走 Server Action