Prisma 实践

要点

  • Prisma 是最流行的 TypeScript ORM,schema 用独立的 .prisma 文件定义
  • Prisma Client 提供完整的类型推导,查询 API 直观
  • 关系建模清晰,include 语法简化关联查询
  • 迁移工具 Prisma Migrate 稳定可靠
  • AI 项目可以用 Prisma,但 pgvector 支持需要额外配置
  • Prisma Studio 可视化查看数据,开发调试方便

1. 安装与初始化

pnpm add prisma @prisma/client
pnpx prisma init

这会生成:

prisma/
└── schema.prisma
.env  # DATABASE_URL

2. Schema 定义

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id            String         @id @default(uuid())
  email         String         @unique
  name          String
  metadata      Json?
  conversations Conversation[]
  createdAt     DateTime       @default(now())
  updatedAt     DateTime       @updatedAt
}
 
model Conversation {
  id        String    @id @default(uuid())
  userId    String
  user      User      @relation(fields: [userId], references: [id])
  title     String
  model     String
  messages  Message[]
  createdAt DateTime  @default(now())
 
  @@index([userId])
}
 
model Message {
  id             String       @id @default(uuid())
  conversationId String
  conversation   Conversation @relation(fields: [conversationId], references: [id])
  role           String       // user, assistant, system
  content        String
  tokens         Int
  createdAt      DateTime     @default(now())
 
  @@index([conversationId, createdAt])
}

关键字段说明:

  • @id:主键
  • @default(uuid()):默认值,UUID
  • @unique:唯一约束
  • @relation:关系定义
  • @@index:复合索引
  • Json?:可选的 JSON 字段

3. 生成 Client

pnpx prisma generate

这会根据 schema 生成 Prisma Client,位于 node_modules/@prisma/client

4. 迁移

# 开发环境:生成并应用迁移
pnpx prisma migrate dev --name init
 
# 生产环境:应用已有的迁移
pnpx prisma migrate deploy
 
# 重置数据库(删除所有数据)
pnpx prisma migrate reset

迁移文件存放在 prisma/migrations/ 目录,可以纳入版本控制。

5. 与 Hono 集成

// src/lib/db.ts
import { PrismaClient } from '@prisma/client'
 
export const prisma = new PrismaClient()
 
// 优雅关闭
process.on('beforeExit', async () => {
  await prisma.$disconnect()
})
// src/routes/users.ts
import { Hono } from 'hono'
import { prisma } from '../lib/db'
 
const app = new Hono()
 
// 查询所有用户
app.get('/users', async (c) => {
  const users = await prisma.user.findMany()
  return c.json(users)
})
 
// 查询单个用户
app.get('/users/:id', async (c) => {
  const id = c.req.param('id')
  const user = await prisma.user.findUnique({
    where: { id },
  })
  if (!user) return c.json({ error: 'Not found' }, 404)
  return c.json(user)
})
 
// 创建用户
app.post('/users', async (c) => {
  const body = await c.req.json()
  const user = await prisma.user.create({
    data: body,
  })
  return c.json(user, 201)
})
 
// 更新用户
app.put('/users/:id', async (c) => {
  const id = c.req.param('id')
  const body = await c.req.json()
  const user = await prisma.user.update({
    where: { id },
    data: body,
  })
  return c.json(user)
})
 
// 删除用户
app.delete('/users/:id', async (c) => {
  const id = c.req.param('id')
  await prisma.user.delete({
    where: { id },
  })
  return c.json({ ok: true })
})
 
export default app

6. 关联查询

Prisma 的 include 语法简化关联查询:

// 查询用户及其对话
const userWithConversations = await prisma.user.findUnique({
  where: { id: 'user-1' },
  include: {
    conversations: true,
  },
})
 
// 嵌套查询:用户 → 对话 → 消息
const userWithMessages = await prisma.user.findUnique({
  where: { id: 'user-1' },
  include: {
    conversations: {
      include: {
        messages: {
          orderBy: { createdAt: 'asc' },
        },
      },
    },
  },
})
 
// 只选择某些字段
const userWithEmail = await prisma.user.findUnique({
  where: { id: 'user-1' },
  select: {
    id: true,
    email: true,
  },
})

7. 条件查询

// 等于
await prisma.user.findMany({
  where: { email: '[email protected]' },
})
 
// 包含
await prisma.user.findMany({
  where: { name: { contains: 'Alice' } },
})
 
// 范围
await prisma.user.findMany({
  where: { createdAt: { gte: new Date('2026-01-01') } },
})
 
// 多个条件(AND)
await prisma.user.findMany({
  where: {
    name: { contains: 'Alice' },
    createdAt: { gte: new Date('2026-01-01') },
  },
})
 
// OR 条件
await prisma.user.findMany({
  where: {
    OR: [
      { email: '[email protected]' },
      { email: '[email protected]' },
    ],
  },
})
 
// 关联过滤
await prisma.user.findMany({
  where: {
    conversations: {
      some: {
        model: 'gpt-4',
      },
    },
  },
})

8. 事务

// 简单事务
await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({
    data: { email: '[email protected]', name: 'Alice' },
  })
 
  await tx.conversation.create({
    data: {
      userId: user.id,
      title: 'New Conversation',
      model: 'gpt-4',
    },
  })
})
 
// 批量操作
await prisma.$transaction([
  prisma.user.update({
    where: { id: 'user-1' },
    data: { name: 'Alice Smith' },
  }),
  prisma.conversation.update({
    where: { id: 'conv-1' },
    data: { title: 'Updated Title' },
  }),
])

9. JSON 字段

// 插入 JSON
await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Alice',
    metadata: {
      role: 'admin',
      tags: ['vip', 'beta'],
    },
  },
})
 
// 查询 JSON 字段(PostgreSQL)
const admins = await prisma.user.findMany({
  where: {
    metadata: {
      path: ['role'],
      equals: 'admin',
    },
  },
})

Prisma 的 JSON 查询不如 Drizzle 灵活。复杂的 JSON 查询需要用 $queryRaw

const results = await prisma.$queryRaw`
  SELECT * FROM users
  WHERE metadata->>'role' = 'admin'
`

10. pgvector 支持

Prisma 不原生支持 pgvector,但可以通过扩展或 $queryRaw 使用:

// prisma/schema.prisma
model DocumentEmbedding {
  id          String   @id @default(uuid())
  documentId  String
  chunk       String
  embedding   Unsupported("vector(1536)")  // pgvector 类型
  createdAt   DateTime @default(now())
}
// 相似度查询
const queryEmbedding = [0.1, 0.2, ...]
 
const results = await prisma.$queryRaw`
  SELECT id, chunk, 1 - (embedding <=> ${queryEmbedding}::vector) as similarity
  FROM document_embeddings
  ORDER BY embedding <=> ${queryEmbedding}::vector
  LIMIT 5
`

或者使用 prisma-pg-extensions 等第三方扩展。

11. Prisma Studio

Prisma Studio 是可视化的数据浏览器:

pnpx prisma studio

打开浏览器,可以查看、编辑数据库里的数据。开发调试时非常方便。

12. 性能优化

12.1 选择字段

避免 selectAll,只选择需要的字段:

// ❌ 查询所有字段
const users = await prisma.user.findMany()
 
// ✅ 只选择需要的字段
const users = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    name: true,
  },
})

12.2 避免 N+1

使用 include 而不是循环查询:

// ❌ N+1 问题
const conversations = await prisma.conversation.findMany()
for (const conv of conversations) {
  const messages = await prisma.message.findMany({
    where: { conversationId: conv.id },
  })
}
 
// ✅ 一次查询
const conversations = await prisma.conversation.findMany({
  include: { messages: true },
})

12.3 分页

// Offset 分页
const page = 1
const pageSize = 10
const conversations = await prisma.conversation.findMany({
  skip: (page - 1) * pageSize,
  take: pageSize,
  orderBy: { createdAt: 'desc' },
})
 
// Cursor 分页(推荐)
const conversations = await prisma.conversation.findMany({
  take: 10,
  cursor: {
    id: 'last-id',  // 上一页的最后一条 ID
  },
  orderBy: { createdAt: 'desc' },
})

总结

Prisma 是成熟的 TypeScript ORM,开发体验好,适合关系建模为主的项目。

这一节涉及到的几个实践:

  1. Schema 定义.prisma 文件定义模型和关系
  2. Client 生成prisma generate 生成类型安全的 Client
  3. 迁移管理prisma migrate dev/deploy 管理 schema 变更
  4. CRUD 操作findManycreateupdatedelete
  5. 关联查询include 语法简化嵌套查询
  6. 事务$transaction 支持回调和批量模式
  7. JSON 查询:支持基本查询,复杂查询用 $queryRaw
  8. pgvector:需要 Unsupported 类型和 $queryRaw
  9. Prisma Studio:可视化数据浏览器
  10. 性能优化:选择字段、避免 N+1、分页

Prisma 适合团队 ORM 经验少、需要完善工具链的项目。如果需要灵活的 pgvector 支持或追求极致性能,Drizzle 可能更合适。

下一篇看数据库连接管理——连接池、Edge 环境连接、连接泄漏预防。