数据库集成(Prisma + Drizzle)

Next.js 没有内置数据库层——你需要自己选择 ORM。本章对比 Prisma 和 Drizzle 两大主流方案,讲清 Schema 定义、Migration、查询模式、连接池管理,以及 Serverless/Edge 环境的适配策略。

1. ORM 选型

维度PrismaDrizzle
定位全功能 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 init

3.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 = prisma

3.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 push

drizzle-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 generate

4.3 Migration 最佳实践

  • 每次变更一个小 migration:不要在一个 migration 中做大量变更
  • 先加后删:新增列 → 迁移数据 → 删除旧列(分两次 migration)
  • 默认值:新增 NOT NULL 列必须提供默认值,否则对已有数据会报错
  • 索引:为高频查询条件加索引(workspaceIduserIdcreatedAt
// 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)
PrismaPrisma 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 用 .prisma DSL
  • 查询:Drizzle 关系查询日常用,SQL-like 查询处理复杂聚合
  • Migration:开发用 push,生产用 generate + migrate
  • 连接池:Serverless 必须用连接池代理(Neon/PgBouncer/Prisma Accelerate)
  • Edge:用 HTTP 协议驱动(@neondatabase/serverless@planetscale/database
  • Server Component:直接查数据库,零 API 中间层

下一章讲实时通信。