Skip to content

Prisma ORM 深度解析:TypeScript 生态最声明式的数据库 ORM 方案 ​

在 TypeScript 全栈开发中,数据层的抽象方式直接影响开发体验和代码质量。Drizzle ORM 以"贴近 SQL"的风格赢得了一批拥趸,而 Prisma 则选择了另一条路——声明式 Schema、链式查询 API 和零成本的类型推断。本文将从 Prisma Schema 设计、CRUD 操作、关系处理、性能优化和 Prisma 5 新特性的维度,带你全面掌握这个生态最成熟的 TypeScript ORM。

一、为什么选择 Prisma ​

1.1 ORM 选型三足鼎立 ​

┌──────────────────────────────────────────────────────────────────────┐
│                    TypeScript ORM 生态三强对比                        │
├──────────────────────────┬──────────────────┬────────────────────────┤
│         Prisma           │      Drizzle     │      TypeORM           │
├──────────────────────────┼──────────────────┼────────────────────────┤
│  设计哲学                │                  │                        │
│  声明式 Schema + 生成    │ SQL-like 查询    │ 经典 Active Record     │
│  类型安全的自动补全      │                  │                        │
├──────────────────────────┼──────────────────┼────────────────────────┤
│  Schema 定义             │                  │                        │
│  高度声明式,接近数据    │ 贴近 SQL,       │ 装饰器/类混合          │
│  模型而非表结构          │ 熟悉 SQL 者友好  │                        │
├──────────────────────────┼──────────────────┼────────────────────────┤
│  类型安全                │                  │                        │
│  ⭐⭐⭐⭐⭐ 极致       │ ⭐⭐⭐⭐ 优秀    │ ⭐⭐⭐ 一般            │
│  从 Schema 到 Client     │ 编译期检查       │ 运行时部分类型         │
│  全链路类型推导          │                  │                        │
├──────────────────────────┼──────────────────┼────────────────────────┤
│  数据库支持              │                  │                        │
│  PostgreSQL, MySQL,     │ PostgreSQL,      │ 15+ 数据库             │
│  SQLite, MongoDB,       │ MySQL, SQLite    │                        │
│  CockroachDB, SQL Server│                  │                        │
├──────────────────────────┼──────────────────┼────────────────────────┤
│  迁移系统                │                  │                        │
│  Prisma Migrate         │ 迁移 SQL          │ TypeORM Migrations     │
│  声明式迁移,版本化      │ 手动 SQL 迁移    │                        │
├──────────────────────────┼──────────────────┼────────────────────────┤
│  性能                    │                  │                        │
│  中等(生成代码需优化) │ ⭐⭐⭐⭐⭐ 最优   │ 较好                    │
│  Prisma Client 成熟稳定  │ 贴近 SQL,       │ N+1 需手动处理         │
│                          │ 无额外开销        │                        │
├──────────────────────────┼──────────────────┼────────────────────────┤
│  学习曲线                │                  │                        │
│  ⭐⭐ 友好              │ ⭐⭐⭐ 中等     │ ⭐⭐⭐⭐ 陡峭         │
│  文档优秀,上手快       │ 需了解 SQL       │ 概念多,配置繁琐       │
├──────────────────────────┼──────────────────┼────────────────────────┤
│  适用场景                │                  │                        │
│  TypeScript 全栈、       │ 高性能需求、     │ 复杂业务逻辑、         │
│  快速开发、类型强迫症    │ SQL 优先团队     │ 多数据库兼容           │
└──────────────────────────┴──────────────────┴────────────────────────┘

1.2 Prisma 的核心价值 ​

Prisma 解决三个核心问题:

txt
┌─────────────────────────────────────────────────────────────────────┐
│                         Prisma 的核心价值                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. 类型安全的数据访问                                               │
│     Schema → Prisma Client → 自动补全 → 零运行时错误                 │
│                                                                     │
│  2. 声明式的数据建模                                                │
│     用接近业务模型的语言描述数据,而非 SQL 表结构                     │
│                                                                     │
│  3. 统一的开发工作流                                                │
│     Schema → Migration → Client → Studio,从设计到数据一站式         │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

二、Prisma Schema 设计与进阶用法 ​

2.1 Schema 基础结构 ​

prisma
// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
  // 输出类型定义文件的位置
  output   = "../node_modules/.prisma/client"
}

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

// ===== 数据模型定义 =====

model User {
  id        String   @id @default(cuid())  // 全局唯一 ID
  email     String   @unique               // 唯一索引
  name      String?
  role      Role     @default(USER)
  createdAt DateTime @default(now())       // 自动时间戳
  updatedAt DateTime @updatedAt

  // 关联关系
  posts     Post[]
  profile   Profile?

  // 复合唯一约束
  @@unique([email, provider])

  // 普通索引
  @@index([email])
  @@index([createdAt])
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // 关联关系(外键)
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)

  // 多对多关系(自动生成中间表)
  categories Category[]
}

model Profile {
  id     String @id @default(cuid())
  bio    String?
  avatar String?
  userId String @unique  // 一对一关系

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Category {
  id    String @id @default(cuid())
  name  String @unique
  slug  String @unique

  posts Post[]
}

// ===== 枚举类型 =====
enum Role {
  USER
  ADMIN
  MODERATOR
}

2.2 字段进阶配置 ​

prisma
// ===== 字段类型详解 =====

model AdvancedExample {
  // String 字段
  email    String   @db.VarChar(255)           // 指定数据库类型
  token    String   @db.Text                   // 长文本
  slug     String   @unique                    // 隐式索引
  code     String   @default(uuid())           // 默认值表达式

  // 数值字段
  age      Int      @db.SmallInt               // 小整数
  price    Decimal  @db.Decimal(10, 2)         // 精确小数
  ratio    Float                               // 浮点数
  views    BigInt   @default(0)                // 大整数

  // 时间字段
  created  DateTime @default(now())            // 默认当前时间
  updated  DateTime @updatedAt                 // 自动更新
  deleted  DateTime?                           // 可空字段,软删除
  schedule DateTime @db.Timestamptz            // 时区感知时间戳

  // 布尔与枚举
  isActive Boolean   @default(true)
  status   Status    @default(PENDING)

  // 关系字段(自动创建外键)
  // posts     Post[]    // 一对多
  // profile   Profile?  // 一对一(可选端)

  // Json 字段(PostgreSQL)
  metadata Json      @default("{}")
  tags     String[]  @default([])              // 数组字段

  // 复合类型(PostgreSQL)
  location Unsupported("geography(Point, 4326)")
}

// ===== 默认值与自动生成 =====

model GeneratorExamples {
  // CUID:全球唯一 ID(默认)
  id1 String @id @default(cuid())

  // UUID:标准 UUID
  id2 String @id @default(uuid())

  // 数据库自增
  id3 Int    @id @default(autoincrement())

  // 时间戳
  ts1 DateTime @default(now())
  ts2 DateTime @default(dbgenerated("NOW()"))  // 数据库函数

  // 序列(PostgreSQL)
  id4 Int @id @default(autoincrement())

  // 静态默认值
  name String @default("Anonymous")
  flag Boolean @default(true)
}

2.3 关系建模深度指南 ​

prisma
// ===== 一对多关系 =====
model Author {
  id    String @id @default(cuid())
  name  String
  books Book[]
}

model Book {
  id       String @id @default(cuid())
  title    String
  authorId String
  author   Author @relation(fields: [authorId], references: [id])
}

// ===== 一对一关系 =====
model User {
  id      String  @id @default(cuid())
  email   String  @unique
  profile Profile?
}

model Profile {
  id     String @id @default(cuid())
  bio    String
  userId String @unique  // 一对一必须唯一约束
  user   User   @relation(fields: [userId], references: [id])
}

// ===== 多对多关系(隐式中间表)=====
model Post {
  id         String      @id @default(cuid())
  title      String
  categories Category[]
}

model Category {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[]
}
// 自动生成 _CategoryToPost 中间表

// ===== 多对多关系(显式中间表)=====
model Post {
  id         String           @id @default(cuid())
  title      String
  postTags   PostToTag[]
}

model Tag {
  id      String     @id @default(cuid())
  name    String     @unique
  postTags PostToTag[]
}

// 显式定义中间表,可添加额外字段
model PostToTag {
  postId String
  tagId  String
  note   String?   // 中间表额外字段
  createdAt DateTime @default(now())

  post   Post @relation(fields: [postId], references: [id], onDelete: Cascade)
  tag    Tag  @relation(fields: [tagId], references: [id], onDelete: Cascade)

  @@id([postId, tagId])  // 复合主键
}

// ===== 自引用关系 =====
// 评论系统:评论可以回复评论
model Comment {
  id        String    @id @default(cuid())
  content   String
  parentId  String?
  parent    Comment?  @relation("CommentReplies", fields: [parentId], references: [id])
  replies   Comment[] @relation("CommentReplies")
  postId    String

  post      Post      @relation(fields: [postId], references: [id], onDelete: Cascade)
}

2.4 Prisma Client 生成与类型系统 ​

typescript
// Prisma Client 一旦生成,就会得到完整的类型定义
// 运行: npx prisma generate

import { PrismaClient } from '@prisma/client';

// 实例化 Prisma Client(单例模式)
const prisma = new PrismaClient();

// 自动推导的类型示例
async function examples() {
  // 1. 查询结果类型自动推导
  const user = await prisma.user.findUnique({
    where: { id: 'xxx' }
  });
  // user 的类型是: User | null
  // 访问 user.email 时,TypeScript 知道这是 string | null

  // 2. 创建参数类型自动推导
  const newUser = await prisma.user.create({
    data: {
      email: 'alice@example.com',
      name: 'Alice',
      role: 'ADMIN',  // TypeScript 知道只能取 Role 枚举值
      profile: {
        create: {
          bio: 'Hello'
        }
      }
    }
  });
  // newUser 的类型是: User(包含嵌套创建的 profile)

  // 3. 联合类型处理
  const result = await prisma.user.findFirst({
    where: { role: 'ADMIN' }
  });
  if (result) {
    // TypeScript 知道 result 不为 null
    console.log(result.email);
  }

  // 4. select 字段裁剪
  const partial = await prisma.user.findUnique({
    where: { id: 'xxx' },
    select: { id: true, email: true }
  });
  // partial 的类型是: { id: string; email: string } | null
  // TypeScript 不允许访问 partial.name
}

三、CRUD 操作实战 ​

3.1 查询(Read) ​

typescript
// src/queries.ts - Prisma 查询实战

import { PrismaClient, Prisma } from '@prisma/client';

const prisma = new PrismaClient();

// ===== 基础查询 =====

async function basicQueries() {
  // 单条查询
  const user = await prisma.user.findUnique({
    where: { id: 'clx1234567890' },
    // select: { id: true, email: true },  // 只返回指定字段
    // omit: { token: true },              // Prisma 5 新特性:排除字段
  });

  // 按唯一字段查询
  const byEmail = await prisma.user.findUnique({
    where: { email: 'alice@example.com' }
  });

  // 列表查询
  const allUsers = await prisma.user.findMany({
    where: {
      role: 'USER',
      createdAt: { gte: new Date('2024-01-01') }
    },
    orderBy: { createdAt: 'desc' },
    take: 10,       // LIMIT 10
    skip: 0,        // OFFSET 0
    distinct: ['role']  // DISTINCT
  });

  // 聚合查询
  const stats = await prisma.user.aggregate({
    _count: { _all: true },
    _avg: { age: true },
    _sum: { age: true },
    where: { role: 'USER' }
  });

  // 分组聚合(Prisma 5.17+)
  const roleStats = await prisma.$queryRaw`
    SELECT role, COUNT(*) as count
    FROM "User"
    GROUP BY role
  `;

  // 分页查询
  async function paginate(page: number, pageSize: number) {
    const skip = (page - 1) * pageSize;
    const [data, total] = await Promise.all([
      prisma.user.findMany({
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' }
      }),
      prisma.user.count()
    ]);

    return {
      data,
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
        hasNext: page * pageSize < total,
        hasPrev: page > 1
      }
    };
  }
}

// ===== 条件查询(过滤)=====

async function advancedFiltering() {
  // AND 条件(默认)
  const andQuery = await prisma.post.findMany({
    where: {
      AND: [
        { published: true },
        { title: { contains: 'TypeScript' } }
      ]
    }
  });

  // OR 条件
  const orQuery = await prisma.post.findMany({
    where: {
      OR: [
        { title: { contains: 'React' } },
        { title: { contains: 'Vue' } },
        { title: { contains: 'Angular' } }
      ]
    }
  });

  // NOT 条件
  const notQuery = await prisma.user.findMany({
    where: {
      NOT: { role: 'ADMIN' }
    }
  });

  // 字符串操作符
  const stringOps = await prisma.post.findMany({
    where: {
      title: {
        equals: 'Hello World',          // 精确匹配
        // contains: 'Hello',            // 包含
        // startsWith: 'Hello',          // 开头
        // endsWith: 'World',            // 结尾
        // mode: 'insensitive',          // 大小写不敏感(PostgreSQL)
      }
    }
  });

  // 数值/日期比较
  const numericOps = await prisma.post.findMany({
    where: {
      createdAt: {
        gte: new Date('2024-01-01'),    // 大于等于
        lte: new Date('2024-12-31'),    // 小于等于
        // gt: new Date(),
        // lt: new Date(),
        // not: new Date(),
      }
    }
  });

  // 数组查询(PostgreSQL)
  const arrayOps = await prisma.user.findMany({
    where: {
      skills: {
        has: 'TypeScript'               // 数组包含
        // hasEvery: ['A', 'B'],         // 包含所有
        // hasSome: ['A', 'B'],          // 包含任一
        // isEmpty: false,               // 非空
      }
    }
  });

  // JSON 查询(PostgreSQL)
  const jsonOps = await prisma.user.findMany({
    where: {
      metadata: {
        path: ['role'],
        equals: 'premium'
      }
    }
  });

  // 关系过滤
  const relationFilter = await prisma.post.findMany({
    where: {
      author: {
        role: 'ADMIN',
        email: { contains: '@company.com' }
      },
      categories: {
        some: {
          name: { in: ['Tech', 'AI'] }
        }
      }
    }
  });
}

3.2 创建(Create) ​

typescript
// ===== 基础创建 =====

async function createExamples() {
  // 单条创建
  const user = await prisma.user.create({
    data: {
      email: 'bob@example.com',
      name: 'Bob',
      role: 'USER'
    }
  });

  // 批量创建
  const users = await prisma.user.createMany({
    data: [
      { email: 'user1@example.com', name: 'User 1' },
      { email: 'user2@example.com', name: 'User 2' },
      { email: 'user3@example.com', name: 'User 3' },
    ],
    skipDuplicates: true  // 跳过已存在的(根据唯一约束)
  });

  // 嵌套创建(一对一)
  const userWithProfile = await prisma.user.create({
    data: {
      email: 'alice@example.com',
      name: 'Alice',
      profile: {
        create: {
          bio: 'Developer',
          avatar: 'https://example.com/avatar.jpg'
        }
      }
    },
    include: { profile: true }  // 返回时包含关联数据
  });

  // 嵌套创建(一对多)
  const authorWithPosts = await prisma.user.create({
    data: {
      email: 'author@example.com',
      name: 'Author',
      posts: {
        create: [
          { title: 'Post 1', content: 'Content 1', published: true },
          { title: 'Post 2', content: 'Content 2', published: false }
        ]
      }
    },
    include: { posts: true }
  });

  // 嵌套创建(多对多)
  const postWithCategories = await prisma.post.create({
    data: {
      title: 'Understanding Prisma',
      content: 'A deep dive into Prisma ORM',
      authorId: authorWithPosts.id,
      categories: {
        connectOrCreate: [
          {
            where: { id: 'existing-category-id' },
            create: { name: 'TypeScript', slug: 'typescript' }
          },
          {
            where: { name: 'ORM' },  // 按 name 查找
            create: { name: 'ORM', slug: 'orm' }
          }
        ]
      }
    },
    include: { categories: true }
  });

  // 使用 connect(关联已存在的记录)
  const connectedPost = await prisma.post.create({
    data: {
      title: 'Another Post',
      author: { connect: { id: user.id } },
      categories: { connect: [{ id: 'category-id' }] }
    }
  });

  // upsert:存在则更新,不存在则创建
  const upsertedUser = await prisma.user.upsert({
    where: { email: 'alice@example.com' },
    update: {
      name: 'Alice Updated',
      updatedAt: new Date()
    },
    create: {
      email: 'alice@example.com',
      name: 'Alice',
      role: 'USER'
    }
  });
}

3.3 更新(Update) ​

typescript
// ===== 基础更新 =====

async function updateExamples() {
  // 单条更新
  const updated = await prisma.user.update({
    where: { id: 'user-id' },
    data: {
      name: 'New Name',
      role: 'ADMIN'
    }
  });

  // 批量更新
  const batchUpdate = await prisma.user.updateMany({
    where: { role: 'USER', createdAt: { lt: new Date('2023-01-01') } },
    data: {
      role: 'LEGACY_USER'
    }
  });

  // 数字字段递增/递减(原子操作)
  const incremented = await prisma.post.update({
    where: { id: 'post-id' },
    data: {
      viewCount: { increment: 1 }
      // decrement: 1
      // multiply: 2
      // divide: 2
    }
  });

  // 字段置空
  const cleared = await prisma.user.update({
    where: { id: 'user-id' },
    data: {
      name: null  // 直接设置为 null
    }
  });

  // 嵌套更新(一对一)
  const userUpdated = await prisma.user.update({
    where: { id: 'user-id' },
    data: {
      name: 'Updated Name',
      profile: {
        update: {
          bio: 'Updated bio'
        }
      }
    }
  });

  // 嵌套更新(一对多)
  const postUpdated = await prisma.post.update({
    where: { id: 'post-id' },
    data: {
      published: true,
      categories: {
        set: [{ id: 'new-category-id' }]  // 替换关联
        // disconnect: [{ id: 'old-category-id' }]  // 断开关联
        // connect: [{ id: 'new-category-id' }]  // 添加关联
      }
    }
  });

  // 使用 raw update(复杂 SQL)
  const rawUpdate = await prisma.$executeRaw`
    UPDATE "User"
    SET "score" = "score" + ${10}
    WHERE "role" = 'USER'
  `;

  // $executeRaw vs $queryRaw
  // $executeRaw → 返回受影响的行数(INSERT/UPDATE/DELETE)
  // $queryRaw → 返回查询结果(SELECT)
}

3.4 删除(Delete) ​

typescript
// ===== 删除操作 =====

async function deleteExamples() {
  // 单条删除(Cascade 会自动删除关联记录)
  const deleted = await prisma.user.delete({
    where: { id: 'user-id' }
  });

  // 批量删除
  const batchDelete = await prisma.user.deleteMany({
    where: {
      email: { endsWith: '@temp.com' },
      createdAt: { lt: new Date('2024-01-01') }
    }
  });

  // 软删除(推荐做法)
  const softDeleted = await prisma.user.update({
    where: { id: 'user-id' },
    data: {
      deletedAt: new Date(),
      // 或者使用一个 isDeleted 字段
      // isDeleted: true
    }
  });

  // 配合 WhereUnique 防止误删关键数据
  const safeDelete = await prisma.$transaction(async (tx) => {
    // 1. 先检查是否可以删除
    const user = await tx.user.findUnique({
      where: { id: 'user-id' },
      include: { posts: true }
    });

    if (!user) throw new Error('User not found');
    if (user.posts.length > 0) {
      throw new Error('Cannot delete user with posts');
    }

    // 2. 执行删除
    return tx.user.delete({ where: { id: 'user-id' } });
  });

  // 强制删除(绕过软删除检查的场景)
  // 先 update 再 delete
  await prisma.$transaction([
    prisma.user.update({
      where: { id: 'user-id' },
      data: { deletedAt: null }  // 清除软删除标记
    }),
    prisma.user.delete({
      where: { id: 'user-id' }
    })
  ]);
}

四、关系查询与 N+1 问题 ​

4.1 include 与 select ​

typescript
// ===== 关联数据加载 =====

async function relationQueries() {
  // 加载关联数据(include)
  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 }  // 嵌套 select
      },
      profile: true
    }
  });
  // userWithPosts.posts 是 Post[],且只包含指定字段

  // 多层嵌套
  const postsWithAuthorsAndCategories = await prisma.post.findMany({
    where: { published: true },
    include: {
      author: {
        select: { id: true, name: true, email: true }
      },
      categories: {
        select: { id: true, name: true }
      }
    }
  });

  // 计数(不需要加载完整数据)
  const userWithPostCount = await prisma.user.findMany({
    include: {
      _count: {
        select: { posts: true, profile: true }
      }
    }
  });
  // user._count.posts 是数字,不是 Post[]

  // 嵌套 include(多级关系)
  const complexQuery = await prisma.user.findMany({
    where: { role: 'ADMIN' },
    include: {
      posts: {
        include: {
          categories: true,
          author: { select: { name: true } }
        }
      }
    }
  });
}

4.2 N+1 问题与解决方案 ​

N+1 问题是最常见的性能杀手:

txt
N+1 问题示例:
假设查询 10 个 User,每个 User 有 5 篇 Post

❌ N+1(101 次查询):
prisma.user.findMany({ take: 10 })
  → 1 次查询获取用户
  → 每个用户单独查询 posts(10 次)
  → 每个 post 单独查询 author(50 次)
  → 每个 post 单独查询 categories(50 次)
  → 总计: 1 + 10 + 50 + 50 = 111 次查询!

✅ 使用 include(1 次查询):
prisma.user.findMany({
  take: 10,
  include: {
    posts: {
      include: { author: true, categories: true }
    }
  }
})
  → 1 次查询完成所有关联数据
typescript
// ===== 避免 N+1 的策略 =====

async function avoidNPlusOne() {
  // 策略 1:使用 include 一次性加载
  const users = await prisma.user.findMany({
    take: 10,
    include: {
      posts: {
        where: { published: true },
        include: { categories: true }
      }
    }
  });

  // 策略 2:使用 findMany + map(需要多次查询但可控)
  // 适合分页场景
  const page = await prisma.user.findMany({
    take: 10,
    skip: 0
  });

  const posts = await prisma.post.findMany({
    where: {
      authorId: { in: page.map(u => u.id) },
      published: true
    }
  });

  // 按 authorId 分组
  const postsByAuthorId = posts.reduce((acc, post) => {
    if (!acc[post.authorId]) acc[post.authorId] = [];
    acc[post.authorId].push(post);
    return acc;
  }, {} as Record<string, typeof posts>);

  // 策略 3:使用 $transaction 打包多次查询
  const [users, posts, categories] = await prisma.$transaction([
    prisma.user.findMany({ take: 10 }),
    prisma.post.findMany({ where: { published: true } }),
    prisma.category.findMany()
  ]);
  // 3 次查询,事务保证一致性

  // 策略 4:Prisma 5 的 batch loading(自动优化)
  // Prisma Client 会自动批处理同一事务内的相同类型查询
}

4.3 分页与无限滚动 ​

typescript
// src/pagination.ts - 实战分页方案

// ===== 游标分页(Cursor-based,适合大数据量)=====

interface CursorPaginationResult<T> {
  data: T[];
  nextCursor: string | null;
  hasMore: boolean;
}

async function cursorPaginate(
  take: number = 10,
  cursor?: string
): Promise<CursorPaginationResult<Post>> {
  const posts = await prisma.post.findMany({
    take: take + 1,  // 多取一条判断是否有更多
    ...(cursor && {
      cursor: { id: cursor },
      skip: 1  // 跳过游标本身
    }),
    orderBy: { createdAt: 'desc' },
    include: { author: { select: { name: true } } }
  });

  const hasMore = posts.length > take;
  const data = hasMore ? posts.slice(0, -1) : posts;
  const nextCursor = hasMore ? data[data.length - 1].id : null;

  return { data, nextCursor, hasMore };
}

// API 使用示例
// GET /api/posts?take=10&cursor=xxx
// 返回 { data: [...], nextCursor: "yyy", hasMore: true }

// ===== 无限滚动分页(前端)=====

async function infiniteScroll(limit: number = 10) {
  const posts = await prisma.post.findMany({
    take: limit,
    orderBy: { createdAt: 'desc' },
    include: { author: true }
  });

  return posts.map(post => ({
    id: post.id,
    title: post.title,
    authorName: post.author.name,
    createdAt: post.createdAt.toISOString()
  }));
}

// ===== 搜索 + 分页组合 =====

interface SearchParams {
  query?: string;
  category?: string;
  page: number;
  pageSize: number;
}

async function searchWithPagination(params: SearchParams) {
  const { query, category, page, pageSize } = params;
  const skip = (page - 1) * pageSize;

  const where: Prisma.PostWhereInput = {
    published: true,
    ...(query && {
      OR: [
        { title: { contains: query, mode: 'insensitive' } },
        { content: { contains: query, mode: 'insensitive' } }
      ]
    }),
    ...(category && {
      categories: {
        some: { slug: category }
      }
    })
  };

  const [posts, total] = await prisma.$transaction([
    prisma.post.findMany({
      where,
      skip,
      take: pageSize,
      orderBy: { createdAt: 'desc' },
      include: {
        author: { select: { name: true, avatar: true } },
        categories: { select: { name: true, slug: true } },
        _count: { select: { comments: true } }
      }
    }),
    prisma.post.count({ where })
  ]);

  return {
    posts,
    pagination: {
      page,
      pageSize,
      total,
      totalPages: Math.ceil(total / pageSize)
    }
  };
}

五、事务与原子操作 ​

5.1 事务基础 ​

typescript
// ===== Prisma 事务 =====

async function transactionExamples() {
  // 方式 1:$transaction(自动提交)
  const result = await prisma.$transaction(async (tx) => {
    // 创建用户
    const user = await tx.user.create({
      data: {
        email: 'alice@example.com',
        name: 'Alice',
        profile: { create: { bio: 'Developer' } }
      }
    });

    // 创建帖子
    const post = await tx.post.create({
      data: {
        title: 'Hello World',
        content: 'My first post',
        authorId: user.id
      }
    });

    // 更新用户帖子数
    // 注意:这里没法直接更新关联字段,需要单独查询
    const updated = await tx.user.update({
      where: { id: user.id },
      data: { name: 'Alice Updated' }
    });

    return { user, post, updated };
  });

  // 方式 2:交互式事务(Interactive Transactions)
  // 允许你在事务中多次与数据库交互,并在最后决定提交或回滚
  const interactiveResult = await prisma.$transaction(async (tx) => {
    const user = await tx.user.create({
      data: { email: 'bob@example.com', name: 'Bob' }
    });

    // 中间可以插入其他逻辑
    const validation = await validateUserData(user);
    if (!validation.valid) {
      throw new Error(validation.error);  // 自动回滚
    }

    return user;
  }, {
    timeout: 10,  // 事务超时(秒)
    isolationLevel: 'Serializable'  // 隔离级别
  });

  // 方式 3:批次事务(Batch Transactions)
  // Prisma 5.10+ 支持
  await prisma.$transaction([
    prisma.user.update({ where: { id: '1' }, data: { name: 'A' } }),
    prisma.user.update({ where: { id: '2' }, data: { name: 'B' } }),
    prisma.user.update({ where: { id: '3' }, data: { name: 'C' } }),
  ], {
    isolationLevel: 'ReadCommitted'
  });
}

5.2 实战:转账与库存扣减 ​

typescript
// ===== 转账(经典事务场景)=====

async function transferMoney(
  fromUserId: string,
  toUserId: string,
  amount: number
) {
  return await prisma.$transaction(async (tx) => {
    // 1. 检查源账户余额
    const fromAccount = await tx.account.findUnique({
      where: { userId: fromUserId }
    });

    if (!fromAccount || fromAccount.balance < amount) {
      throw new Error('Insufficient balance');
    }

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

    // 3. 存款
    await tx.account.update({
      where: { userId: toUserId },
      data: { balance: { increment: amount } }
    });

    // 4. 记录流水
    await tx.transactionLog.create({
      data: {
        fromUserId,
        toUserId,
        amount,
        type: 'TRANSFER'
      }
    });

    return { success: true };
  }, {
    isolationLevel: 'Serializable'  // 最高隔离级别,防止并发问题
  });
}

// ===== 库存扣减(防止超卖)=====

async function reduceInventory(productId: string, quantity: number) {
  return await prisma.$transaction(async (tx) => {
    // 使用 SELECT FOR UPDATE 锁定行(需要数据库支持)
    // Prisma 不直接支持 SELECT FOR UPDATE,但可以通过 raw query 实现
    const result = await tx.$executeRaw`
      UPDATE "Product"
      SET "stock" = "stock" - ${quantity}
      WHERE "id" = ${productId}
        AND "stock" >= ${quantity}
    `;

    if (result === 0) {
      throw new Error('Insufficient stock or product not found');
    }

    // 记录库存变动
    await tx.inventoryLog.create({
      data: {
        productId,
        change: -quantity,
        reason: 'ORDER'
      }
    });

    return { success: true, remainingStock: result };
  }, {
    isolationLevel: 'Serializable'
  });
}

六、迁移系统与数据库版本管理 ​

6.1 Prisma Migrate 工作流 ​

bash
# ===== 迁移命令 =====

# 1. 初始化(首次创建 schema 后)
npx prisma migrate dev --name init

# 2. 开发阶段:修改 schema 后生成迁移
npx prisma migrate dev --name add_user_bio

# 3. 生产环境:应用迁移
npx prisma migrate deploy

# 4. 查看迁移状态
npx prisma migrate status

# 5. 重置数据库(危险!开发环境)
npx prisma migrate reset

# 6. 创建空迁移(手动编写 SQL)
npx prisma migrate dev --name custom_migration --create-only
# 然后手动编辑 prisma/migrations/xxxx_custom_migration/migration.sql

6.2 迁移文件结构 ​

sql
-- prisma/migrations/20240101000000_add_user_bio/migration.sql

-- CreateTable
CREATE TABLE "Profile" (
    "id" TEXT NOT NULL,
    "bio" TEXT,
    "avatar" TEXT,
    "userId" TEXT NOT NULL,

    CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId");

-- AddForeignKey
ALTER TABLE "Profile"
  ADD CONSTRAINT "Profile_userId_fkey"
  FOREIGN KEY ("userId")
  REFERENCES "User"(id)
  ON DELETE CASCADE
  ON UPDATE CASCADE;

-- AddForeignKey(反向:User → Profile)
ALTER TABLE "User"
  ADD COLUMN "profileId" TEXT;

ALTER TABLE "User"
  ADD CONSTRAINT "User_profileId_fkey"
  FOREIGN KEY ("profileId")
  REFERENCES "Profile"(id)
  ON DELETE SET NULL
  ON UPDATE CASCADE;

6.3 生产环境迁移策略 ​

typescript
// ===== 生产环境安全迁移策略 =====

// 策略 1:蓝绿部署
// 1. 在 staging 环境测试迁移
// 2. 部署新代码(包含迁移脚本)
// 3. 新 Pod 启动时自动执行 prisma migrate deploy
// 4. 验证无误后,切换流量到新 Pod

// 策略 2:使用 Reset(需配合数据备份)
// 适用于 schema 大改、可接受停机的情况

// 策略 3:渐进式迁移
// 1. 添加新字段(nullable)
// 2. 部署代码使用新字段
// 3. 后台数据迁移
// 4. 删除旧字段

// 示例:添加一个可空的新字段
// schema.prisma:
// model User {
//   ...
//   newField String?  // 可空,允许部署
// }

// 迁移后,运行数据迁移脚本
// npx ts-node scripts/migrate-data.ts

// scripts/migrate-data.ts
async function migrateData() {
  let migrated = 0;
  const batchSize = 1000;
  let skip = 0;

  while (true) {
    const users = await prisma.user.findMany({
      where: { newField: null },
      take: batchSize,
      skip
    });

    if (users.length === 0) break;

    for (const user of users) {
      await prisma.user.update({
        where: { id: user.id },
        data: { newField: computeNewField(user) }
      });
    }

    migrated += users.length;
    skip += batchSize;
    console.log(`Migrated ${migrated} records...`);
  }

  console.log(`Migration complete: ${migrated} records`);
}

七、Prisma 5 新特性 ​

prisma
// Prisma 5 的关键改进

generator client {
  provider = "prisma-client-js"
  // Prisma 5: 预发布功能启用
  previewFeatures = ["relationJoins"]  // 关系 JOIN 优化
}

// 新增:omit 选项(避免返回敏感字段)
model User {
  id       String  @id @default(cuid())
  email    String  @unique
  password String  // 敏感字段,不应返回给前端
  name     String?

  // Prisma 5: 可以在查询时省略字段
}

// 新增:groupBy 增强
// 之前需要 raw query,现在原生支持
typescript
// Prisma 5 新特性示例

async function prisma5Features() {
  // 1. omit(排除字段)
  const user = await prisma.user.findUnique({
    where: { id: 'xxx' },
    omit: {
      password: true,  // 不返回密码
      // token: true,
    }
  });
  // user 类型自动不包含 password 字段!

  // 2. $transaction 支持数组形式(批量事务)
  const results = await prisma.$transaction([
    prisma.user.create({ data: { email: 'a@test.com', name: 'A' } }),
    prisma.user.create({ data: { email: 'b@test.com', name: 'B' } }),
    prisma.user.create({ data: { email: 'c@test.com', name: 'C' } }),
  ]);
  // 返回所有创建结果的数组

  // 3. groupBy 原生支持
  const stats = await prisma.user.groupBy({
    by: ['role'],
    _count: { _all: true },
    where: { deletedAt: null }
  });
  // 无需使用 $queryRaw

  // 4. findMany 支持 having(需要 Prisma 5.17+)
  const roles = await prisma.user.findMany({
    where: { role: 'ADMIN' },
    having: { id: { _count: { gt: 5 } } }
  });

  // 5. 更好的错误信息
  // Prisma 5 改进了 ManyNotRelated 和其他常见错误提示
}

八、性能优化实战 ​

8.1 Prisma Client 配置 ​

typescript
// src/lib/prisma.ts - Prisma Client 单例配置

import { PrismaClient } from '@prisma/client';
import { Prisma } from '@prisma/client';

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

// 开发环境:全局复用(避免热重载创建多个连接)
// 生产环境:使用单例模式
export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === 'development'
      ? ['query', 'info', 'warn', 'error']
      : ['error'],

    // 连接池配置
    datasources: {
      db: {
        url: process.env.DATABASE_URL + '?connection_limit=10&pool_timeout=20'
      }
    }
  });

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

// ===== 扩展 Prisma Client(添加自定义方法)=====

export const extendedPrisma = prisma.$extends({
  model: {
    user: {
      // 添加自定义查询方法
      async findByEmail(email: string) {
        return this.findUnique({
          where: { email }
        });
      },

      async findAdmins() {
        return this.findMany({
          where: { role: 'ADMIN' }
        });
      },

      // 添加自定义写入方法
      async createWithProfile(data: {
        email: string;
        name: string;
        bio: string;
      }) {
        return this.create({
          data: {
            email: data.email,
            name: data.name,
            profile: {
              create: { bio: data.bio }
            }
          }
        });
      }
    },

    post: {
      async publish(id: string) {
        return this.update({
          where: { id },
          data: { published: true }
        });
      },

      async unpublish(id: string) {
        return this.update({
          where: { id },
          data: { published: false }
        });
      }
    }
  },

  result: {
    user: {
      // 添加计算字段
      fullName: {
        needs: { name: true, email: true },
        compute(user) {
          return user.name || user.email.split('@')[0];
        }
      }
    }
  }
});

// 使用扩展
async function useExtendedPrisma() {
  const admin = await extendedPrisma.user.findAdmins();
  const user = await extendedPrisma.user.createWithProfile({
    email: 'alice@example.com',
    name: 'Alice',
    bio: 'Developer'
  });
  console.log(user.fullName);  // 自定义计算字段
}

8.2 索引与查询优化 ​

prisma
// ===== Schema 索引优化 =====

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String   @db.Text
  published Boolean  @default(false)
  authorId  String
  createdAt DateTime @default(now())

  // 单字段索引
  @@index([authorId])

  // 多字段复合索引(查询优化)
  @@index([published, createdAt])
  @@index([authorId, published])
}

// 对于高频查询,使用复合索引
// 例如: WHERE published = true ORDER BY createdAt DESC
// → 复合索引 [published, createdAt] 最优
typescript
// ===== 查询优化检查清单 =====

async function queryOptimization() {
  // ✅ 使用 select/omit 减少数据传输
  const ids = await prisma.user.findMany({
    select: { id: true }
  });

  // ✅ 只加载需要的关联
  const users = await prisma.user.findMany({
    include: { profile: true }  // 不要 include 所有关联
  });

  // ✅ 使用 take 限制数量
  const recentUsers = await prisma.user.findMany({
    orderBy: { createdAt: 'desc' },
    take: 100  // 永远不要 select * 而不加 limit
  });

  // ✅ 使用 findFirst/findUnique 而非 findMany
  const firstAdmin = await prisma.user.findFirst({
    where: { role: 'ADMIN' }
  });

  // ✅ 使用 count 而非 length(findMany())
  const userCount = await prisma.user.count({
    where: { role: 'USER' }
  });

  // ✅ 使用 exists 而非 count > 0
  const hasAdmins = await prisma.user.findFirst({
    where: { role: 'ADMIN' }
  }).then(user => !!user);

  // Prisma 5.6+: 使用 $exists
  const adminExists = await prisma.user.findFirst({
    where: { role: 'ADMIN' }
  });

  // ✅ 避免在循环中查询
  // 错误 ❌:
  for (const userId of userIds) {
    const posts = await prisma.post.findMany({
      where: { authorId: userId }
    });
  }

  // 正确 ✅:
  const allPosts = await prisma.post.findMany({
    where: { authorId: { in: userIds } }
  });
  const postsByAuthor = groupBy(allPosts, 'authorId');
}

8.3 Raw Query 使用指南 ​

typescript
// ===== Prisma Raw Query =====

async function rawQueryExamples() {
  // $queryRaw → SELECT,返回结果
  const users = await prisma.$queryRaw`
    SELECT id, email, name
    FROM "User"
    WHERE "role" = ${'ADMIN'}
    ORDER BY "createdAt" DESC
    LIMIT 10
  `;

  // $queryRawUnsafe → 参数化查询(需要手动防注入)
  const unsafeUsers = await prisma.$queryRawUnsafe(
    'SELECT * FROM "User" WHERE email = $1',
    ['alice@example.com']
  );

  // $executeRaw → INSERT/UPDATE/DELETE,返回影响行数
  const updateCount = await prisma.$executeRaw`
    UPDATE "User"
    SET "name" = 'Updated'
    WHERE "role" = ${'USER'}
  `;

  // 复杂聚合查询
  const monthlyStats = await prisma.$queryRaw<Array<{
    month: string;
    user_count: bigint;
    post_count: bigint;
  }>>`
    SELECT
      TO_CHAR(DATE_TRUNC('month', "createdAt"), 'YYYY-MM') as month,
      COUNT(DISTINCT "User".id)::bigint as user_count,
      COUNT(DISTINCT "Post".id)::bigint as post_count
    FROM "User"
    LEFT JOIN "Post" ON "Post"."authorId" = "User".id
    GROUP BY DATE_TRUNC('month', "createdAt")
    ORDER BY month DESC
  `;

  // 全文搜索(PostgreSQL)
  const searchResults = await prisma.$queryRaw`
    SELECT * FROM "Post"
    WHERE to_tsvector('english', title || ' ' || COALESCE(content, ''))
          @@ plainto_tsquery('english', ${searchQuery})
    ORDER BY ts_rank(
      to_tsvector('english', title || ' ' || COALESCE(content, '')),
      plainto_tsquery('english', ${searchQuery})
    ) DESC
    LIMIT 20
  `;
}

九、实战:构建完整的数据访问层 ​

typescript
// src/repositories/user.repository.ts - 仓储模式

import { Prisma } from '@prisma/client';
import { prisma } from '@/lib/prisma';

export interface CreateUserDto {
  email: string;
  name?: string;
  role?: 'USER' | 'ADMIN' | 'MODERATOR';
}

export interface UpdateUserDto {
  name?: string;
  role?: 'USER' | 'ADMIN' | 'MODERATOR';
}

export interface UserFilters {
  role?: 'USER' | 'ADMIN' | 'MODERATOR';
  search?: string;
  page?: number;
  pageSize?: number;
}

export class UserRepository {
  async findById(id: string) {
    return prisma.user.findUnique({
      where: { id },
      include: { profile: true, _count: { select: { posts: true } } }
    });
  }

  async findByEmail(email: string) {
    return prisma.user.findUnique({
      where: { email },
      include: { profile: true }
    });
  }

  async findAll(filters: UserFilters) {
    const { role, search, page = 1, pageSize = 20 } = filters;
    const skip = (page - 1) * pageSize;

    const where: Prisma.UserWhereInput = {
      deletedAt: null,
      ...(role && { role }),
      ...(search && {
        OR: [
          { name: { contains: search, mode: 'insensitive' } },
          { email: { contains: search, mode: 'insensitive' } }
        ]
      })
    };

    const [users, total] = await prisma.$transaction([
      prisma.user.findMany({
        where,
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
        include: {
          profile: { select: { avatar: true } },
          _count: { select: { posts: true } }
        }
      }),
      prisma.user.count({ where })
    ]);

    return {
      data: users,
      pagination: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) }
    };
  }

  async create(dto: CreateUserDto) {
    return prisma.user.create({
      data: {
        email: dto.email,
        name: dto.name,
        role: dto.role || 'USER'
      }
    });
  }

  async update(id: string, dto: UpdateUserDto) {
    return prisma.user.update({
      where: { id },
      data: dto
    });
  }

  async delete(id: string) {
    // 软删除
    return prisma.user.update({
      where: { id },
      data: { deletedAt: new Date() }
    });
  }

  async hardDelete(id: string) {
    // 硬删除(物理删除)
    return prisma.user.delete({
      where: { id }
    });
  }

  async toggleRole(id: string) {
    const user = await prisma.user.findUnique({ where: { id } });
    if (!user) throw new Error('User not found');

    const roleMap = {
      USER: 'ADMIN',
      ADMIN: 'MODERATOR',
      MODERATOR: 'USER'
    };

    return prisma.user.update({
      where: { id },
      data: { role: roleMap[user.role] }
    });
  }
}

export const userRepository = new UserRepository();

十、总结与选型建议 ​

Prisma 适用场景:

✅ 强烈推荐使用 Prisma:
├─ TypeScript 全栈项目(Next.js、Nuxt、NestJS)
├─ 快速原型开发,需要快速迭代
├─ 团队成员对 SQL 不熟悉
├─ 需要极致的类型安全
├─ PostgreSQL/MySQL/SQLite 为数据源
└─ 需要内建迁移系统

⚠️ 考虑其他方案:
├─ 高性能大数据量 → Drizzle ORM
├─ 复杂业务逻辑 → TypeORM 或直接 SQL
├─ 多数据库兼容 → TypeORM
└─ MongoDB 为主 → Mongoose 或 Prisma(MongoDB preview)

Prisma vs Drizzle 选型决策树:

                  团队 SQL 熟悉度?
                        │
         ┌──────────────┴──────────────┐
         ▼                             ▼
    不熟悉 SQL                    熟悉 SQL
         │                             │
         ▼                             ▼
    Prisma(类型安全优先)        需要极致性能?
         │                             │
         │                      ┌──────┴──────┐
         │                      ▼             ▼
         │                   是              否
         │                    │              │
         │                    ▼              ▼
         │               Drizzle         两者皆可
         │                               看团队偏好
         │
         ▼
    Prisma(文档好,上手快)

Prisma 以其声明式的 Schema 设计、极致的类型安全性和优秀的开发体验,成为 TypeScript 生态中最受欢迎的 ORM 方案。配合 Prisma Studio 的可视化管理和 Prisma Migrate 的版本化迁移,构建数据层从未如此简单。


本文由小虾子 🦐 撰写

Last updated: