Prisma
问题
Prisma 的核心特性和使用方法是什么?与传统 ORM 有什么不同?
答案
一、Prisma 概述
Prisma 是 Node.js/TypeScript 生态中最流行的数据库工具包,由三部分组成:
与传统 ORM 的核心区别:
| 特性 | Prisma | 传统 ORM(如 TypeORM) |
|---|---|---|
| Schema 定义 | .prisma 文件(DSL) | TypeScript 装饰器 / 类 |
| 类型生成 | 根据 Schema 自动生成 | 手动定义 Entity |
| 查询 API | 声明式对象 | 方法链 / Query Builder |
| SQL 控制 | $queryRaw 逃逸 | QueryBuilder 灵活构建 |
| 关系加载 | 显式 include | @Relation + 懒加载 |
二、Prisma Schema
prisma/schema.prisma
// 数据源配置
datasource db {
provider = "postgresql" // 支持 postgresql、mysql、sqlite、mongodb、cockroachdb
url = env("DATABASE_URL")
}
// 客户端生成器
generator client {
provider = "prisma-client-js"
}
// 用户模型
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
age Int @default(0)
role Role @default(USER)
posts Post[] // 一对多关系
profile Profile? // 一对一关系
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email]) // 索引
@@map("users") // 映射表名
}
// 文章模型
model Post {
id Int @id @default(autoincrement())
title String
content String? @db.Text
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
tags Tag[] // 多对多(隐式关系表)
createdAt DateTime @default(now())
}
// 用户档案(一对一)
model Profile {
id Int @id @default(autoincrement())
bio String
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
// 标签
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
// 角色枚举
enum Role {
USER
ADMIN
}
Schema 设计要点
@id定义主键,@unique定义唯一约束@default(autoincrement())自增主键,@default(uuid())UUID 主键@relation必须指定外键字段(fields)和引用字段(references)?表示可选字段,对应 SQL 的NULL@@index定义复合索引,@@unique定义复合唯一约束
三、CRUD 操作
创建
// 创建单条
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
// 同时创建关联数据(嵌套创建)
profile: {
create: { bio: 'I am Alice' }
},
posts: {
create: [
{ title: 'First Post' },
{ title: 'Second Post' }
]
}
},
// 指定返回字段
include: { profile: true, posts: true }
});
// 批量创建
const count = await prisma.user.createMany({
data: [
{ email: 'bob@example.com', name: 'Bob' },
{ email: 'charlie@example.com', name: 'Charlie' },
],
skipDuplicates: true, // 跳过重复记录
});
查询
// 查找唯一记录
const user = await prisma.user.findUnique({
where: { email: 'alice@example.com' }
});
// 条件查询
const users = await prisma.user.findMany({
where: {
age: { gte: 18 }, // >= 18
name: { contains: 'li' }, // LIKE '%li%'
email: { endsWith: '@gmail.com' },
OR: [ // OR 条件
{ role: 'ADMIN' },
{ posts: { some: { published: true } } } // 存在已发布文章
]
},
include: { posts: true }, // 加载关联
orderBy: { createdAt: 'desc' }, // 排序
skip: 0, // 偏移(分页)
take: 10, // 限制数量
});
// 聚合查询
const stats = await prisma.user.aggregate({
_avg: { age: true },
_count: { id: true },
_max: { age: true },
});
// 分组查询
const grouped = await prisma.user.groupBy({
by: ['role'],
_count: { id: true },
_avg: { age: true },
});
更新
// 更新单条
const user = await prisma.user.update({
where: { id: 1 },
data: {
name: 'Alice Updated',
age: { increment: 1 }, // 原子自增
}
});
// Upsert(不存在则创建)
const user = await prisma.user.upsert({
where: { email: 'alice@example.com' },
create: { email: 'alice@example.com', name: 'Alice' },
update: { name: 'Alice Updated' },
});
// 批量更新
await prisma.user.updateMany({
where: { role: 'USER' },
data: { age: { increment: 1 } },
});
删除
// 删除单条
await prisma.user.delete({ where: { id: 1 } });
// 批量删除
await prisma.user.deleteMany({
where: { createdAt: { lt: new Date('2023-01-01') } }
});
四、关系查询
// include:加载完整关联对象
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
where: { published: true }, // 过滤关联数据
orderBy: { createdAt: 'desc' },
take: 5,
}
}
});
// select:精确选择字段(减少数据传输)
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
name: true,
posts: {
select: { id: true, title: true }
}
}
});
select vs include
include:加载关联对象的所有字段select:精确选择需要的字段(包括关联对象的字段)- 两者不能同时在同一层使用
五、数据库迁移
# 创建迁移(根据 Schema 变更生成 SQL)
npx prisma migrate dev --name add_user_age
# 生成的迁移文件:
# prisma/migrations/20240101_add_user_age/migration.sql
# ALTER TABLE "users" ADD COLUMN "age" INTEGER NOT NULL DEFAULT 0;
# 应用到生产环境
npx prisma migrate deploy
# 重置数据库
npx prisma migrate reset
# 生成 Client(修改 Schema 后)
npx prisma generate
六、事务
// 交互式事务(推荐)
const result = await prisma.$transaction(async (tx) => {
// tx 是事务中的 Prisma Client
const user = await tx.user.create({ data: { email: 'a@b.com', name: 'A' } });
const post = await tx.post.create({
data: { title: 'Hello', authorId: user.id }
});
return { user, post };
});
// 批量操作事务
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: 'a@b.com', name: 'A' } }),
prisma.post.create({ data: { title: 'Hello', authorId: 1 } }),
]);
七、原始 SQL
// 原始查询(参数化,防 SQL 注入)
const users = await prisma.$queryRaw`
SELECT u.*, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id
HAVING COUNT(p.id) > ${minPosts}
`;
// 原始执行
await prisma.$executeRaw`
UPDATE users SET age = age + 1 WHERE role = 'USER'
`;
八、性能优化
// 1. 使用 select 减少数据传输
const users = await prisma.user.findMany({
select: { id: true, name: true } // 只查需要的字段
});
// 2. 使用 cursor 分页(大数据集)
const page = await prisma.post.findMany({
take: 20,
skip: 1,
cursor: { id: lastPostId }, // 基于游标分页,比 offset 高效
orderBy: { id: 'asc' },
});
// 3. 连接池配置
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// connection_limit=10&connect_timeout=5
}
常见面试问题
Q1: Prisma 的类型安全是怎么实现的?
答案:
Prisma 通过 代码生成 实现类型安全:
- 开发者编写
.prismaSchema 文件 prisma generate读取 Schema,生成 TypeScript 类型定义(@prisma/client)- 生成的 Client 为每个模型提供完全类型化的 API
// 自动生成的类型 —— 开发者无需手动定义
type User = { id: number; email: string; name: string | null; ... };
// 查询参数也是类型安全的
prisma.user.findMany({
where: { email: 123 } // ❌ 类型错误:email 应为 string
});
Q2: Prisma 如何处理 N+1 问题?
答案:
Prisma 不支持懒加载,强制使用显式的 include / select 来加载关联数据。这种设计从根本上避免了意外的 N+1:
// 不使用 include,posts 字段根本不会出现在返回结果中
const user = await prisma.user.findUnique({ where: { id: 1 } });
// user.posts → 不存在此属性
// 显式加载关联数据
const user = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true } // 只有这样才能获取 posts
});
Q3: Prisma 的 Migrate 和 db push 有什么区别?
答案:
| 命令 | 用途 | 特点 |
|---|---|---|
prisma migrate dev | 生成可追溯的迁移文件 | 生产环境推荐,有迁移历史 |
prisma db push | 直接同步 Schema 到数据库 | 原型开发,不生成迁移文件 |
- 开发/生产环境:使用
migrate dev/migrate deploy - 原型快速迭代:使用
db push
Q4: Prisma 的局限性?
答案:
- 复杂 SQL:窗口函数、递归 CTE 等必须用
$queryRaw - Schema 限制:不支持数据库视图(View)、存储过程
- 多数据源:不支持同一 Client 连接多个数据库
- 迁移灵活性:数据迁移需手写 SQL(仅 Schema 迁移自动化)