Chapter 05

Prisma ORM 现代数据库操作

类型安全的数据库操作、声明式 Schema、自动迁移——告别手写 SQL 的噩梦

为什么选择 Prisma

JavaScript 世界的 ORM 不少——Sequelize(老牌)、TypeORM(装饰器风格)、Drizzle(轻量 SQL)……Prisma 的独特之处在于:

安装与初始化

# 安装 Prisma CLI 和客户端
bun add prisma -d
bun add @prisma/client

# 初始化(生成 prisma/schema.prisma)
bunx prisma init --datasource-provider postgresql
# 或 sqlite / mysql / mongodb
# .env — 数据库连接
DATABASE_URL="postgresql://postgres:password@localhost:5432/mydb?schema=public"

# SQLite(开发/测试首选)
DATABASE_URL="file:./dev.db"

Prisma Schema 语法

Schema 文件(prisma/schema.prisma)是 Prisma 的核心,用 PSL(Prisma Schema Language)描述:

// 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      Role     @default(USER)
  password  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // 关系字段
  posts     Post[]
  profile   Profile?

  @@index([email])  // 为邮箱建索引
  @@map("users")    // 映射到数据库表名
}

// 枚举
enum Role {
  USER
  ADMIN
  MODERATOR
}

// 个人资料(一对一)
model Profile {
  id     String  @id @default(cuid())
  bio    String?
  avatar String?
  userId String  @unique
  user   User    @relation(fields: [userId], references: [id], onDelete: Cascade)
}

// 文章(多对多标签)
model Post {
  id          String   @id @default(cuid())
  title       String
  content     String?
  published   Boolean  @default(false)
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])
  tags        Tag[]    @relation("PostToTag")  // 多对多
  createdAt   DateTime @default(now())

  @@index([authorId])
}

model Tag {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[] @relation("PostToTag")
}

数据库迁移

# 开发环境:创建并应用迁移
bunx prisma migrate dev --name add_user_table
# 这会:1. 生成 SQL 迁移文件  2. 应用迁移  3. 重新生成 Prisma Client

# 查看迁移历史
bunx prisma migrate status

# 生产环境:只应用已有迁移(不生成新迁移)
bunx prisma migrate deploy

# 重置数据库(危险!删除所有数据)
bunx prisma migrate reset

# 直接推送 Schema(不生成迁移文件,适合原型期)
bunx prisma db push

# 打开 Prisma Studio(可视化数据库管理)
bunx prisma studio

迁移文件要提交到 gitprisma/migrations/ 目录下的 SQL 文件是数据库历史的记录,必须提交。这样团队成员和 CI 都能重现相同的数据库状态。

Prisma Client — CRUD 操作

初始化客户端(单例模式)

// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

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

基础 CRUD

import { prisma } from './lib/prisma';

// ─── CREATE ───
const user = await prisma.user.create({
  data: {
    email: 'zhang@example.com',
    name: '张三',
    password: hashedPassword,
    profile: {
      create: { bio: '全栈工程师' }  // 同时创建关联数据
    }
  },
  include: { profile: true },  // 返回包含 profile 的完整对象
});

// 批量创建
await prisma.user.createMany({
  data: [
    { email: 'a@example.com', name: '用户A', password: 'hash1' },
    { email: 'b@example.com', name: '用户B', password: 'hash2' },
  ],
  skipDuplicates: true,
});

// ─── READ ───
const user = await prisma.user.findUnique({
  where: { email: 'zhang@example.com' },
  select: { id: true, name: true, email: true }, // 只查询需要的字段
});

const users = await prisma.user.findMany({
  where: {
    role: 'USER',
    createdAt: { gte: new Date('2024-01-01') },
    OR: [
      { name: { contains: '张' } },
      { email: { endsWith: '@gmail.com' } },
    ],
  },
  orderBy: [{ createdAt: 'desc' }, { name: 'asc' }],
  skip: 20,  // 分页:跳过前20条
  take: 10,  // 每页10条
});

// ─── UPDATE ───
const updated = await prisma.user.update({
  where: { id: 'user-id' },
  data: { name: '新名字', updatedAt: new Date() },
});

// upsert = update or insert
const upserted = await prisma.user.upsert({
  where: { email: 'zhang@example.com' },
  update: { name: '更新名字' },
  create: { email: 'zhang@example.com', name: '张三', password: 'hash' },
});

// ─── DELETE ───
await prisma.user.delete({ where: { id: 'user-id' } });
await prisma.user.deleteMany({ where: { role: 'USER', createdAt: { lt: cutoffDate } } });

关系查询

// 一对多:查询用户及其所有文章
const userWithPosts = await prisma.user.findUnique({
  where: { id: 'user-id' },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      take: 5,
      select: { id: true, title: true, createdAt: true },
    },
    profile: true,
  },
});

// 多对多:查询文章及其标签
const post = await prisma.post.findUnique({
  where: { id: 'post-id' },
  include: { tags: true, author: { select: { name: true, email: true } } },
});

// 连接多对多关系
await prisma.post.update({
  where: { id: 'post-id' },
  data: {
    tags: {
      connect: [{ id: 'tag-1' }, { id: 'tag-2' }],
      disconnect: [{ id: 'tag-old' }],
    }
  }
});

// 聚合查询
const stats = await prisma.post.aggregate({
  _count: { id: true },
  where: { published: true },
});
// stats._count.id — 已发布文章总数

// groupBy
const postsByAuthor = await prisma.post.groupBy({
  by: ['authorId'],
  _count: { id: true },
  where: { published: true },
  orderBy: { _count: { id: 'desc' } },
});

事务

// 方式1:$transaction 数组(最简单)
const [newUser, newProfile] = await prisma.$transaction([
  prisma.user.create({ data: { email: 'a@b.com', password: 'hash' } }),
  prisma.profile.create({ data: { bio: 'Hello', userId: 'will-be-filled' } }),
]);

// 方式2:交互式事务(可包含条件逻辑)
const result = await prisma.$transaction(async (tx) => {
  // 检查余额
  const sender = await tx.account.findUnique({ where: { id: senderId } });
  if (!sender || sender.balance < amount) {
    throw new Error('余额不足'); // 自动回滚
  }

  // 扣款
  await tx.account.update({
    where: { id: senderId },
    data: { balance: { decrement: amount } },
  });

  // 收款
  await tx.account.update({
    where: { id: receiverId },
    data: { balance: { increment: amount } },
  });

  // 记录流水
  return tx.transaction.create({
    data: { senderId, receiverId, amount, type: 'TRANSFER' },
  });
}, {
  maxWait: 5000,   // 等待连接最多 5 秒
  timeout: 10000, // 事务超时 10 秒
});

Prisma Accelerate — 连接池与缓存

Serverless 和边缘环境的最大问题是数据库连接数爆炸(每个函数实例都创建新连接)。Prisma Accelerate 解决了这个问题:

// 安装 Accelerate 扩展
bun add @prisma/extension-accelerate
import { PrismaClient } from '@prisma/client/edge';  // 使用 Edge 版本
import { withAccelerate } from '@prisma/extension-accelerate';

const prisma = new PrismaClient().$extends(withAccelerate());

// 带缓存的查询
const users = await prisma.user.findMany({
  cacheStrategy: {
    ttl: 60,       // 缓存 60 秒
    swr: 600,      // stale-while-revalidate:后台刷新时可使用旧缓存
  },
});

Drizzle ORM 的崛起:如果你觉得 Prisma 太重(需要独立的守护进程、Schema 文件),可以考虑 Drizzle ORM——它用纯 TypeScript 定义表结构,无代码生成,bundle size 更小,更适合 Cloudflare Workers 等边缘环境。