Skip to content

Prisma 核心概念

一、Schema 定义

prisma/schema.prisma 是 Prisma 的核心,同时承担三个职责:

  1. 定义数据库连接
  2. 描述数据模型(等同 TypeORM Entity)
  3. 配置客户端生成器
prisma
// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

// Prisma 7.x:数据源 URL 在 prisma.config.ts 中配置,不在 schema 里
datasource db {
  provider = "postgresql"
}

// ── 枚举 ──────────────────────────────────────
enum Role {
  USER
  ADMIN
  MODERATOR
}

// ── 模型 ──────────────────────────────────────
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?                    // ? = 可为空(nullable)
  password  String
  role      Role     @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt       // 自动更新时间戳
  deletedAt DateTime?                 // 软删除

  posts     Post[]

  @@index([email, role])              // 复合索引
  @@map("users")                      // 数据库表名
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  authorId  Int
  createdAt DateTime @default(now())

  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  tags      Tag[]    @relation("PostTags")
  comments  Comment[]

  @@index([authorId])
  @@map("posts")
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[] @relation("PostTags")

  @@map("tags")
}

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  postId    Int
  authorId  Int
  createdAt DateTime @default(now())

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

  @@map("comments")
}

生成客户端:

bash
npx prisma generate

二、Prisma Service(NestJS 集成)

typescript
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

// prisma/prisma.module.ts
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

三、完整 CRUD 示例

typescript
@Injectable()
export class PostsService {
  constructor(private prisma: PrismaService) {}

  // ── 查询 ──────────────────────────────────────

  async findAll(options?: {
    authorId?: number;
    published?: boolean;
    page?: number;
    limit?: number;
  }) {
    const { authorId, published, page = 1, limit = 10 } = options ?? {};

    const where = {
      ...(authorId ? { authorId } : {}),
      ...(published !== undefined ? { published } : {}),
      deletedAt: null,  // 排除软删除
    };

    const [items, total] = await this.prisma.$transaction([
      this.prisma.post.findMany({
        where,
        include: {
          author: { select: { id: true, name: true, email: true } },
          tags: true,
          _count: { select: { comments: true } },  // 关联计数
        },
        orderBy: { createdAt: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.post.count({ where }),
    ]);

    return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
  }

  async findOne(id: number) {
    const post = await this.prisma.post.findUnique({
      where: { id },
      include: {
        author: true,
        tags: true,
        comments: {
          include: { author: { select: { id: true, name: true } } },
          orderBy: { createdAt: 'asc' },
        },
      },
    });
    if (!post) throw new NotFoundException(`文章 #${id} 不存在`);
    return post;
  }

  // ── 创建 ──────────────────────────────────────

  async create(dto: CreatePostDto, authorId: number) {
    return this.prisma.post.create({
      data: {
        title: dto.title,
        content: dto.content,
        author: { connect: { id: authorId } },  // 关联已有用户
        tags: {
          connectOrCreate: dto.tagNames?.map(name => ({
            where: { name },
            create: { name },
          })),
        },
      },
      include: { author: true, tags: true },
    });
  }

  // ── 更新 ──────────────────────────────────────

  async update(id: number, dto: UpdatePostDto) {
    await this.findOne(id);  // 确认存在

    return this.prisma.post.update({
      where: { id },
      data: {
        ...dto,
        tags: dto.tagIds ? {
          set: dto.tagIds.map(tagId => ({ id: tagId })),  // 替换所有标签
        } : undefined,
      },
      include: { tags: true },
    });
  }

  // ── 删除 ──────────────────────────────────────

  async remove(id: number) {
    await this.findOne(id);
    // 硬删除
    await this.prisma.post.delete({ where: { id } });

    // 软删除(需要 deletedAt 字段)
    // await this.prisma.post.update({
    //   where: { id },
    //   data: { deletedAt: new Date() },
    // });
  }
}

四、高级查询

嵌套过滤

typescript
// 查询有"已发布文章"的用户
const authors = await prisma.user.findMany({
  where: {
    posts: {
      some: { published: true },  // 至少一篇发布
    },
  },
});

// 查询所有文章都已发布的用户
const allPublishedAuthors = await prisma.user.findMany({
  where: {
    posts: {
      every: { published: true },  // 每篇都发布
    },
  },
});

// 查询没有文章的用户
const usersWithoutPosts = await prisma.user.findMany({
  where: {
    posts: { none: {} },
  },
});

关联计数与聚合

typescript
// 统计各角色用户数
const stats = await prisma.user.groupBy({
  by: ['role'],
  _count: { id: true },
  _min: { createdAt: true },
});

// 查询并附带关联数量
const postsWithCount = await prisma.post.findMany({
  include: {
    _count: {
      select: { comments: true, tags: true },
    },
  },
});
// postsWithCount[0]._count.comments → 评论数

原始 SQL

typescript
// 复杂查询无法用 Prisma Client 表达时
const result = await prisma.$queryRaw<{ id: number; avgRating: number }[]>`
  SELECT p.id, AVG(r.score) as "avgRating"
  FROM posts p
  JOIN ratings r ON r.post_id = p.id
  WHERE p.published = true
  GROUP BY p.id
  HAVING AVG(r.score) > 4.0
  ORDER BY "avgRating" DESC
  LIMIT 10
`;

// 参数化查询(防止 SQL 注入)
const keyword = '%nestjs%';
const posts = await prisma.$queryRaw`
  SELECT * FROM posts WHERE title ILIKE ${keyword}
`;

五、中间件(Prisma Middleware)

Prisma 支持对所有查询进行拦截:

typescript
// 软删除中间件:自动过滤 deletedAt 不为 null 的记录
prisma.$use(async (params, next) => {
  if (params.model === 'Post') {
    if (params.action === 'findUnique' || params.action === 'findFirst') {
      params.action = 'findFirst';
      params.args.where = { ...params.args.where, deletedAt: null };
    }
    if (params.action === 'findMany') {
      params.args.where = { ...params.args.where, deletedAt: null };
    }
  }
  return next(params);
});

// 查询日志中间件
prisma.$use(async (params, next) => {
  const before = Date.now();
  const result = await next(params);
  const after = Date.now();
  console.log(`${params.model}.${params.action} - ${after - before}ms`);
  return result;
});

六、与 TypeORM 的主要差异

操作TypeORMPrisma
查询关联{ relations: ['author'] }{ include: { author: true } }
分页.skip(n).take(m){ skip: n, take: m }
创建关联repo.create(dto); repo.save(entity)prisma.post.create({ data: { author: { connect: { id } } } })
更新关联save(entity) 处理关联{ tags: { set: [...] } } 替换
原生 SQLquery() / QueryBuilder$queryRaw`...`
软删除@DeleteDateColumn() + 自动处理手动字段 + 中间件
事务DataSource.transaction()$transaction([...])$transaction(async tx => ...)
类型推断关系字段可能是 any完整类型推断

七、常见陷阱

陷阱 1:未运行 prisma generate

bash
# ❌ 修改 schema.prisma 后忘记重新生成客户端
# → 代码中 prisma.newModel 报类型错误

# ✅ 每次修改 schema 后必须运行
npx prisma generate

陷阱 2:$transaction 数组 vs 交互式

typescript
// 数组形式:操作并行提交,无法在操作间做条件判断
const [a, b] = await prisma.$transaction([
  prisma.user.create({ data: userDto }),
  prisma.post.create({ data: postDto }),  // userDto 的 id 在这里不可用!
]);

// 交互式事务:顺序执行,可以使用前一步的结果
await prisma.$transaction(async tx => {
  const user = await tx.user.create({ data: userDto });
  await tx.post.create({ data: { ...postDto, authorId: user.id } });  // ✅
});

陷阱 3:N+1 查询与 select 的关系

typescript
// ❌ 多次查询同一关联(N+1)
const posts = await prisma.post.findMany();
for (const post of posts) {
  const author = await prisma.user.findUnique({ where: { id: post.authorId } });
  // N 篇文章 = N 次额外查询
}

// ✅ 一次 include 获取所有关联
const posts = await prisma.post.findMany({
  include: { author: true },  // 单次 JOIN 查询
});

可运行 Demo: practice/03-crud-app/prisma-version — Prisma Schema、Client、迁移工作流实战


常见错误

错误原因解决
prisma generate 后类型不更新IDE 缓存未刷新重启 TypeScript 服务或重启 IDE
P2002 Unique constraint failed唯一键冲突捕获 PrismaClientKnownRequestErrore.code === 'P2002' 时抛出 ConflictException
P2025 Record not foundfindUniqueOrThrow 找不到记录捕获后抛出 NotFoundException,或改用 findUnique 手动判断 null
prisma migrate deploy 失败迁移文件被修改不要修改已提交的迁移文件;需要修改时创建新迁移
Prisma Client 实例过多每次创建 new PrismaClient()使用单例 PrismaService,注册为 NestJS Provider

NestJS 深度学习体系