跳到主要内容

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 通过 代码生成 实现类型安全:

  1. 开发者编写 .prisma Schema 文件
  2. prisma generate 读取 Schema,生成 TypeScript 类型定义(@prisma/client
  3. 生成的 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 的局限性?

答案

  1. 复杂 SQL:窗口函数、递归 CTE 等必须用 $queryRaw
  2. Schema 限制:不支持数据库视图(View)、存储过程
  3. 多数据源:不支持同一 Client 连接多个数据库
  4. 迁移灵活性:数据迁移需手写 SQL(仅 Schema 迁移自动化)

相关链接