数据库集成(Prisma + Drizzle)
Next.js 没有内置数据库层——你需要自己选择 ORM。本章对比 Prisma 和 Drizzle 两大主流方案,讲清 Schema 定义、Migration、查询模式、连接池管理,以及 Serverless/Edge 环境的适配策略。
1. ORM 选型
| 维度 | Prisma | Drizzle |
|---|---|---|
| 定位 | 全功能 ORM | 轻量 SQL 映射 |
| Schema 定义 | .prisma 文件(DSL) | TypeScript 代码 |
| 查询风格 | 自定义 API | 类 SQL(贴近原生) |
| 类型推断 | 自动生成 @prisma/client | 从 Schema 直接推断 |
| Bundle 体积 | 大(~2MB,含引擎) | 小(~50KB) |
| Edge Runtime | 需要 Prisma Accelerate | 原生支持 |
| 生态 | 成熟,社区大 | 增长快,API 稳定 |
推荐:新项目优先 Drizzle——体积小、类型推断好、Edge 兼容。已有 Prisma 项目不必迁移。
2. Drizzle 集成
2.1 安装与配置
pnpm add drizzle-orm @neondatabase/serverless
pnpm add -D drizzle-kit// drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
})2.2 Schema 定义
Drizzle 的 Schema 就是普通 TypeScript——IDE 直接提供类型补全和跳转:
// src/db/schema.ts
import { pgTable, uuid, text, timestamp, integer, boolean, pgEnum } from 'drizzle-orm/pg-core'
export const roleEnum = pgEnum('role', ['user', 'assistant', 'system'])
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const workspaces = pgTable('workspaces', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
type: text('type', { enum: ['personal', 'team'] }).notNull().default('personal'),
ownerId: uuid('owner_id').references(() => users.id).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const chats = pgTable('chats', {
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
model: text('model').notNull().default('gpt-4o'),
workspaceId: uuid('workspace_id').references(() => workspaces.id).notNull(),
userId: uuid('user_id').references(() => users.id).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
export const messages = pgTable('messages', {
id: uuid('id').defaultRandom().primaryKey(),
chatId: uuid('chat_id').references(() => chats.id, { onDelete: 'cascade' }).notNull(),
role: roleEnum('role').notNull(),
content: text('content').notNull(),
tokenCount: integer('token_count'),
createdAt: timestamp('created_at').defaultNow().notNull(),
})2.3 数据库客户端
// src/db/index.ts
import { drizzle } from 'drizzle-orm/neon-serverless'
import { Pool } from '@neondatabase/serverless'
import * as schema from './schema'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
export const db = drizzle(pool, { schema })2.4 查询模式
Drizzle 提供两种查询风格:
关系查询(推荐日常使用)——类似 Prisma 的 findMany:
// 查询对话列表(含消息数量)
const chatList = await db.query.chats.findMany({
where: eq(chats.workspaceId, workspaceId),
orderBy: desc(chats.updatedAt),
limit: 20,
offset: (page - 1) * 20,
with: {
messages: {
columns: { id: true },
},
},
})
// 查询单个对话(含所有消息)
const chat = await db.query.chats.findFirst({
where: eq(chats.id, chatId),
with: {
messages: {
orderBy: asc(messages.createdAt),
},
},
})使用关系查询需要定义 relations:
// src/db/schema.ts — 关系定义
import { relations } from 'drizzle-orm'
export const chatsRelations = relations(chats, ({ many, one }) => ({
messages: many(messages),
user: one(users, { fields: [chats.userId], references: [users.id] }),
workspace: one(workspaces, { fields: [chats.workspaceId], references: [workspaces.id] }),
}))
export const messagesRelations = relations(messages, ({ one }) => ({
chat: one(chats, { fields: [messages.chatId], references: [chats.id] }),
}))SQL-like 查询(复杂场景)——更贴近原生 SQL,适合聚合、子查询:
import { sql, eq, desc, count } from 'drizzle-orm'
// 统计每个模型的对话数
const modelStats = await db
.select({
model: chats.model,
chatCount: count(chats.id),
})
.from(chats)
.where(eq(chats.workspaceId, workspaceId))
.groupBy(chats.model)
.orderBy(desc(count(chats.id)))
// Token 消耗统计
const tokenUsage = await db
.select({
date: sql<string>`DATE(${messages.createdAt})`,
totalTokens: sql<number>`SUM(${messages.tokenCount})`,
})
.from(messages)
.innerJoin(chats, eq(messages.chatId, chats.id))
.where(eq(chats.workspaceId, workspaceId))
.groupBy(sql`DATE(${messages.createdAt})`)
.orderBy(sql`DATE(${messages.createdAt})`)3. Prisma 集成
3.1 安装与初始化
pnpm add prisma @prisma/client
npx prisma init3.2 Schema 定义
Prisma 用自己的 DSL(.prisma 文件):
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(uuid())
email String @unique
name String?
chats Chat[]
createdAt DateTime @default(now()) @map("created_at")
@@map("users")
}
model Chat {
id String @id @default(uuid())
title String
model String @default("gpt-4o")
userId String @map("user_id")
user User @relation(fields: [userId], references: [id])
messages Message[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("chats")
}
model Message {
id String @id @default(uuid())
chatId String @map("chat_id")
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
role Role
content String
tokenCount Int? @map("token_count")
createdAt DateTime @default(now()) @map("created_at")
@@map("messages")
}
enum Role {
user
assistant
system
}3.3 客户端单例
Serverless 环境中每个请求可能创建新的 Prisma Client——需要用全局单例避免连接泄漏:
// src/db/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 = prisma3.4 查询示例
// Prisma 查询风格
const chats = await prisma.chat.findMany({
where: { userId: session.user.id },
include: { messages: { orderBy: { createdAt: 'asc' } } },
orderBy: { updatedAt: 'desc' },
take: 20,
skip: (page - 1) * 20,
})4. Migration
4.1 Drizzle Migration
# 生成 migration 文件
pnpm drizzle-kit generate
# 执行 migration
pnpm drizzle-kit migrate
# 开发时快速同步(不生成 migration 文件,直接 push)
pnpm drizzle-kit pushdrizzle-kit push 适合开发阶段快速迭代,generate + migrate 适合生产部署——migration 文件可以 code review 和版本控制。
4.2 Prisma Migration
# 生成并执行 migration
npx prisma migrate dev --name add_token_count
# 生产部署
npx prisma migrate deploy
# 生成 Prisma Client
npx prisma generate4.3 Migration 最佳实践
- 每次变更一个小 migration:不要在一个 migration 中做大量变更
- 先加后删:新增列 → 迁移数据 → 删除旧列(分两次 migration)
- 默认值:新增 NOT NULL 列必须提供默认值,否则对已有数据会报错
- 索引:为高频查询条件加索引(
workspaceId、userId、createdAt)
// Drizzle 索引示例
import { index } from 'drizzle-orm/pg-core'
export const chats = pgTable('chats', {
// ...字段定义
}, (table) => [
index('chats_workspace_id_idx').on(table.workspaceId),
index('chats_updated_at_idx').on(table.updatedAt),
])5. 连接池与 Serverless 适配
5.1 问题
Serverless 函数每次冷启动都可能创建新的数据库连接。高并发时连接数爆炸,超出数据库限制。
5.2 解决方案
方案一:连接池代理
使用 Neon(内置连接池)、PgBouncer、Supabase Connection Pooler 等外部连接池:
// Neon — 自动适配 Serverless
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql, { schema })方案二:Prisma Accelerate
Prisma 的托管连接池 + 全球边缘缓存:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // Prisma Accelerate URL
directUrl = env("DIRECT_DATABASE_URL") // 直连(用于 migration)
}5.3 Edge Runtime 兼容
Edge Runtime 不支持 Node.js 原生 TCP 连接——需要用 HTTP 协议的数据库驱动:
| 数据库服务 | Edge 兼容驱动 |
|---|---|
| Neon | @neondatabase/serverless(HTTP) |
| PlanetScale | @planetscale/database(HTTP) |
| Turso (SQLite) | @libsql/client(HTTP) |
| Prisma | Prisma Accelerate(HTTP) |
| Supabase | @supabase/supabase-js(HTTP) |
// Edge 兼容的 Drizzle 客户端(Neon HTTP)
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
// 这个可以在 Edge Runtime 和 Middleware 中使用
const sql = neon(process.env.DATABASE_URL!)
export const edgeDb = drizzle(sql, { schema })6. 在 Server Component 中使用
数据库查询直接在 Server Component 中调用——无需 API 层:
// app/(dashboard)/chat/page.tsx
import { db } from '@/src/db'
import { chats, messages } from '@/src/db/schema'
export default async function ChatPage() {
const workspace = await getCurrentWorkspace()
const chatList = await db.query.chats.findMany({
where: eq(chats.workspaceId, workspace!.id),
orderBy: desc(chats.updatedAt),
with: {
messages: {
limit: 1,
orderBy: desc(messages.createdAt),
columns: { content: true },
},
},
})
return <ChatList chats={chatList} />
}这是 App Router 的核心优势——Server Component 直接查数据库,没有 API 中间层,没有序列化开销,没有客户端 waterfall。
本章小结
- 选型:新项目推荐 Drizzle(轻量、Edge 兼容),已有项目继续 Prisma
- Schema:Drizzle 用 TypeScript 定义,Prisma 用
.prismaDSL - 查询:Drizzle 关系查询日常用,SQL-like 查询处理复杂聚合
- Migration:开发用
push,生产用generate+migrate - 连接池:Serverless 必须用连接池代理(Neon/PgBouncer/Prisma Accelerate)
- Edge:用 HTTP 协议驱动(
@neondatabase/serverless、@planetscale/database) - Server Component:直接查数据库,零 API 中间层
下一章讲实时通信。