Prisma 核心概念
一、Schema 定义
prisma/schema.prisma 是 Prisma 的核心,同时承担三个职责:
- 定义数据库连接
- 描述数据模型(等同 TypeORM Entity)
- 配置客户端生成器
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 的主要差异
| 操作 | TypeORM | Prisma |
|---|---|---|
| 查询关联 | { 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: [...] } } 替换 |
| 原生 SQL | query() / 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 | 唯一键冲突 | 捕获 PrismaClientKnownRequestError,e.code === 'P2002' 时抛出 ConflictException |
P2025 Record not found | findUniqueOrThrow 找不到记录 | 捕获后抛出 NotFoundException,或改用 findUnique 手动判断 null |
prisma migrate deploy 失败 | 迁移文件被修改 | 不要修改已提交的迁移文件;需要修改时创建新迁移 |
| Prisma Client 实例过多 | 每次创建 new PrismaClient() | 使用单例 PrismaService,注册为 NestJS Provider |