关系查询
Prisma Client 的一个关键特性是能够查询两个或多个模型之间的关系。关系查询包括
Prisma Client 还提供一个用于遍历关系的流畅 API。
嵌套读取
嵌套读取允许你从数据库中的多个表中读取相关数据,例如一个用户及其帖子。你可以
关系加载策略(预览)
从版本 5.8.0 开始,你可以通过 relationLoadStrategy
选项为 PostgreSQL 数据库决定 Prisma Client 执行关系查询的方式(即应应用何种加载策略)。
从版本 5.10.0 开始,此功能也适用于 MySQL。
由于 relationLoadStrategy
选项目前处于预览阶段,你需要在 Prisma schema 文件中通过 relationJoins
预览特性标志来启用它
generator client {
provider = "prisma-client-js"
previewFeatures = ["relationJoins"]
}
添加此标志后,你需要再次运行 prisma generate
来重新生成 Prisma Client。relationJoins
功能目前在 PostgreSQL, CockroachDB 和 MySQL 上可用。
Prisma Client 支持两种关系加载策略
join
(默认): 使用数据库级别的LATERAL JOIN
(PostgreSQL) 或相关子查询 (MySQL),并通过单个数据库查询获取所有数据。query
: 向数据库发送多个查询(每个表一个),并在应用层进行关联。
这两种选项的另一个重要区别是 join
策略在数据库级别使用 JSON 聚合。这意味着它在数据库中就已创建了 Prisma Client 返回的 JSON 结构,从而节省了应用层的计算资源。
注意:一旦
relationLoadStrategy
从预览阶段进入正式发布阶段,join
将普遍成为所有关系查询的默认策略。
示例
你可以在任何支持 include
或 select
的查询的顶级使用 relationLoadStrategy
选项。
这是一个使用 include
的示例
const users = await prisma.user.findMany({
relationLoadStrategy: 'join', // or 'query'
include: {
posts: true,
},
})
这是另一个使用 select
的示例
const users = await prisma.user.findMany({
relationLoadStrategy: 'join', // or 'query'
select: {
posts: true,
},
})
何时使用哪种加载策略?
- 在大多数场景下,
join
策略(默认)会更有效。在 PostgreSQL 上,它结合使用LATERAL JOINs
和 JSON 聚合来减少结果集的冗余,并将查询结果转换为预期 JSON 结构的工作委托给数据库服务器。在 MySQL 上,它使用相关子查询通过单个查询获取结果。 - 在某些边缘情况下,取决于数据集和查询的特性,
query
可能更具性能。我们建议你分析数据库查询以识别这些情况。 - 如果你想节省数据库服务器上的资源,并将数据合并和转换的繁重工作放在应用服务器上(这可能更容易扩展),则使用
query
。
包含关系
以下示例返回单个用户及其帖子
const user = await prisma.user.findFirst({
include: {
posts: true,
},
})
包含特定关系的所有字段
以下示例返回一篇帖子及其作者
const post = await prisma.post.findFirst({
include: {
author: true,
},
})
包含深度嵌套关系
你可以嵌套 include
选项来包含关系的关联。以下示例返回一个用户的帖子,以及每篇帖子的类别
const user = await prisma.user.findFirst({
include: {
posts: {
include: {
categories: true,
},
},
},
})
选择包含关系中的特定字段
你可以使用嵌套的 select
来选择要返回的关系字段子集。例如,以下查询返回用户的 name
和每篇相关帖子的 title
const user = await prisma.user.findFirst({
select: {
name: true,
posts: {
select: {
title: true,
},
},
},
})
你也可以在 include
内部嵌套 select
- 以下示例返回所有 User
字段和每篇帖子的 title
字段
const user = await prisma.user.findFirst({
include: {
posts: {
select: {
title: true,
},
},
},
})
请注意,你不能在同一级别使用 select
和 include
。这意味着,如果你选择 include
一个用户的帖子并 select
每篇帖子的标题,你就不能只 select
用户的 email
// The following query returns an exception
const user = await prisma.user.findFirst({
select: { // This won't work!
email: true
}
include: { // This won't work!
posts: {
select: {
title: true
}
}
},
})
取而代之的是,使用嵌套的 select
选项
const user = await prisma.user.findFirst({
select: {
// This will work!
email: true,
posts: {
select: {
title: true,
},
},
},
})
关系计数
在 3.0.1 及更高版本中,你可以与字段一起include
或 select
关系的计数,例如,用户的帖子数量。
const relationCount = await prisma.user.findMany({
include: {
_count: {
select: { posts: true },
},
},
})
过滤关系列表
当你使用 select
或 include
返回相关数据的子集时,你可以在 select
或 include
内部过滤和排序关系列表。
例如,以下查询返回与用户关联的未发布帖子的标题列表
const result = await prisma.user.findFirst({
select: {
posts: {
where: {
published: false,
},
orderBy: {
title: 'asc',
},
select: {
title: true,
},
},
},
})
你也可以使用 include
写出相同的查询,如下所示
const result = await prisma.user.findFirst({
include: {
posts: {
where: {
published: false,
},
orderBy: {
title: 'asc',
},
},
},
})
嵌套写入
嵌套写入允许你在单个事务中将关系数据写入数据库。
嵌套写入
- 为在单个 Prisma Client 查询中跨多个表创建、更新或删除数据提供事务性保证。如果查询的任何部分失败(例如,创建用户成功但创建帖子失败),Prisma Client 将回滚所有更改。
- 支持数据模型支持的任何嵌套级别。
- 在使用模型的创建或更新查询时,适用于关系字段。以下部分展示了每个查询可用的嵌套写入选项。
创建相关记录
你可以同时创建一条记录和一条或多条相关记录。以下查询创建一条 User
记录和两条相关的 Post
记录
const result = await prisma.user.create({
data: {
email: 'elsa@prisma.io',
name: 'Elsa Prisma',
posts: {
create: [
{ title: 'How to make an omelette' },
{ title: 'How to eat an omelette' },
],
},
},
include: {
posts: true, // Include all posts in the returned object
},
})
创建单个记录和多个相关记录
有两种方法可以创建或更新单个记录和多个相关记录 - 例如,一个有多个帖子的用户
- 使用嵌套的
create
查询 - 使用嵌套的
createMany
查询
在大多数情况下,嵌套的 create
更可取,除非需要 skipDuplicates
查询选项。下面是一个快速表格,描述了这两种选项的区别
特性 | create | createMany | 注意 |
---|---|---|---|
支持嵌套附加关系 | ✔ | ✘ * | 例如,你可以在一个查询中创建一个用户、几篇帖子以及每篇帖子的几条评论。 * 你可以在 has-one 关系中手动设置外键 - 例如: { authorId: 9} |
支持 1-n 关系 | ✔ | ✔ | 例如,你可以创建一个用户和多个帖子(一个用户有多篇帖子) |
支持 m-n 关系 | ✔ | ✘ | 例如,你可以创建一篇帖子和多个类别(一篇帖子可以有多个类别,一个类别也可以有多篇帖子) |
支持跳过重复记录 | ✘ | ✔ | 使用 skipDuplicates 查询选项。 |
使用嵌套的 create
以下查询使用嵌套的 create
来创建
- 一个用户
- 两篇帖子
- 一个帖子类别
该示例还使用嵌套的 include
来在返回的数据中包含所有帖子和帖子类别。
const result = await prisma.user.create({
data: {
email: 'yvette@prisma.io',
name: 'Yvette',
posts: {
create: [
{
title: 'How to make an omelette',
categories: {
create: {
name: 'Easy cooking',
},
},
},
{ title: 'How to eat an omelette' },
],
},
},
include: {
// Include posts
posts: {
include: {
categories: true, // Include post categories
},
},
},
})
这是嵌套创建操作如何一次性写入数据库中多个表的视觉表示
使用嵌套的 createMany
以下查询使用嵌套的 createMany
来创建
- 一个用户
- 两篇帖子
该示例还使用嵌套的 include
来在返回的数据中包含所有帖子。
const result = await prisma.user.create({
data: {
email: 'saanvi@prisma.io',
posts: {
createMany: {
data: [{ title: 'My first post' }, { title: 'My second post' }],
},
},
},
include: {
posts: true,
},
})
请注意,在突出显示的查询内部不可能再嵌套额外的 create
或 createMany
,这意味着你不能同时创建用户、帖子和帖子类别。
作为一种变通方法,你可以先发送查询创建将被连接的记录,然后再创建实际记录。例如
const categories = await prisma.category.createManyAndReturn({
data: [
{ name: 'Fun', },
{ name: 'Technology', },
{ name: 'Sports', }
],
select: {
id: true
}
});
const posts = await prisma.post.createManyAndReturn({
data: [{
title: "Funniest moments in 2024",
categoryId: categories.filter(category => category.name === 'Fun')!.id
}, {
title: "Linux or macOS — what's better?",
categoryId: categories.filter(category => category.name === 'Technology')!.id
},
{
title: "Who will win the next soccer championship?",
categoryId: categories.filter(category => category.name === 'Sports')!.id
}]
});
如果你想在单个数据库查询中创建所有记录,考虑使用 $transaction
或 类型安全的原始 SQL。
创建多个记录和多个相关记录
你无法在 createMany()
或 createManyAndReturn()
查询中访问关系,这意味着你无法在单个嵌套写入中创建多个用户和多个帖子。以下操作是不可能的
const createMany = await prisma.user.createMany({
data: [
{
name: 'Yewande',
email: 'yewande@prisma.io',
posts: {
// Not possible to create posts!
},
},
{
name: 'Noor',
email: 'noor@prisma.io',
posts: {
// Not possible to create posts!
},
},
],
})
连接多个记录
以下查询创建 (create
) 一条新的 User
记录,并将该记录 (connect
) 连接到三篇现有帖子
const result = await prisma.user.create({
data: {
email: 'vlad@prisma.io',
posts: {
connect: [{ id: 8 }, { id: 9 }, { id: 10 }],
},
},
include: {
posts: true, // Include all posts in the returned object
},
})
注意:如果任何帖子记录找不到,Prisma Client 将抛出异常:
connect: [{ id: 8 }, { id: 9 }, { id: 10 }]
连接单个记录
你可以将现有记录 connect
到新的或现有用户。以下查询将一篇现有帖子(id: 11
)连接到现有用户(id: 9
)
const result = await prisma.user.update({
where: {
id: 9,
},
data: {
posts: {
connect: {
id: 11,
},
},
},
include: {
posts: true,
},
})
连接或创建记录
如果相关记录可能存在也可能不存在,请使用 connectOrCreate
来连接相关记录
- 连接邮箱地址为
viola@prisma.io
的User
或 - 如果用户不存在,则创建邮箱地址为
viola@prisma.io
的新User
const result = await prisma.post.create({
data: {
title: 'How to make croissants',
author: {
connectOrCreate: {
where: {
email: 'viola@prisma.io',
},
create: {
email: 'viola@prisma.io',
name: 'Viola',
},
},
},
},
include: {
author: true,
},
})
断开相关记录
要从记录列表(例如,一篇特定的博客帖子)中 disconnect
一条记录,请提供要断开连接的记录的 ID 或唯一标识符
const result = await prisma.user.update({
where: {
id: 16,
},
data: {
posts: {
disconnect: [{ id: 12 }, { id: 19 }],
},
},
include: {
posts: true,
},
})
要 disconnect
一条记录(例如,帖子的作者),使用 disconnect: true
const result = await prisma.post.update({
where: {
id: 23,
},
data: {
author: {
disconnect: true,
},
},
include: {
author: true,
},
})
断开所有相关记录
要 disconnect
一对多关系中的所有相关记录(一个用户有多篇帖子),按所示将关系 set
为一个空列表
const result = await prisma.user.update({
where: {
id: 16,
},
data: {
posts: {
set: [],
},
},
include: {
posts: true,
},
})
删除所有相关记录
删除所有相关的 Post
记录
const result = await prisma.user.update({
where: {
id: 11,
},
data: {
posts: {
deleteMany: {},
},
},
include: {
posts: true,
},
})
删除特定相关记录
通过删除所有未发布的帖子来更新用户
const result = await prisma.user.update({
where: {
id: 11,
},
data: {
posts: {
deleteMany: {
published: false,
},
},
},
include: {
posts: true,
},
})
通过删除特定帖子来更新用户
const result = await prisma.user.update({
where: {
id: 6,
},
data: {
posts: {
deleteMany: [{ id: 7 }],
},
},
include: {
posts: true,
},
})
更新所有相关记录(或过滤)
你可以使用嵌套的 updateMany
来更新特定用户的所有相关记录。以下查询将特定用户的所有帖子设为未发布
const result = await prisma.user.update({
where: {
id: 6,
},
data: {
posts: {
updateMany: {
where: {
published: true,
},
data: {
published: false,
},
},
},
},
include: {
posts: true,
},
})
更新特定相关记录
const result = await prisma.user.update({
where: {
id: 6,
},
data: {
posts: {
update: {
where: {
id: 9,
},
data: {
title: 'My updated title',
},
},
},
},
include: {
posts: true,
},
})
更新或创建相关记录
以下查询使用嵌套的 upsert
来更新用户(如果存在邮箱为 "bob@prisma.io"
的用户),如果用户不存在则创建该用户
const result = await prisma.post.update({
where: {
id: 6,
},
data: {
author: {
upsert: {
create: {
email: 'bob@prisma.io',
name: 'Bob the New User',
},
update: {
email: 'bob@prisma.io',
name: 'Bob the existing user',
},
},
},
},
include: {
author: true,
},
})
向现有记录添加新的相关记录
你可以在 update
内部嵌套 create
或 createMany
来向现有记录添加新的相关记录。以下查询向 ID 为 9 的用户添加了两篇帖子
const result = await prisma.user.update({
where: {
id: 9,
},
data: {
posts: {
createMany: {
data: [{ title: 'My first post' }, { title: 'My second post' }],
},
},
},
include: {
posts: true,
},
})
关系过滤器
过滤 "-to-many" 关系
Prisma Client 提供 some
、every
和 none
选项,用于根据关系 "-to-many" 侧相关记录的属性来过滤记录。例如,根据用户的帖子属性过滤用户。
例如
要求 | 要使用的查询选项 |
---|---|
“我想要一个包含至少有一篇未发布 Post 记录的所有 User 的列表” | some 帖子未发布 |
“我想要一个包含没有未发布 Post 记录的所有 User 的列表” | none 帖子未发布 |
“我想要一个包含只有未发布 Post 记录的所有 User 的列表” | every 帖子未发布 |
例如,以下查询返回符合以下条件的 User
- 没有浏览量超过 100 的帖子
- 所有帖子点赞数少于或等于 50
const users = await prisma.user.findMany({
where: {
posts: {
none: {
views: {
gt: 100,
},
},
every: {
likes: {
lte: 50,
},
},
},
},
include: {
posts: true,
},
})
过滤 "-to-one" 关系
Prisma Client 提供 is
和 isNot
选项,用于根据关系 "-to-one" 侧相关记录的属性来过滤记录。例如,根据帖子的作者属性过滤帖子。
例如,以下查询返回符合以下条件的 Post
记录
- 作者姓名不是 Bob
- 作者年龄大于 40
const users = await prisma.post.findMany({
where: {
author: {
isNot: {
name: 'Bob',
},
is: {
age: {
gt: 40,
},
},
},
},
include: {
author: true,
},
})
过滤不存在 "-to-many" 记录的情况
例如,以下查询使用 none
返回所有没有帖子的用户
const usersWithZeroPosts = await prisma.user.findMany({
where: {
posts: {
none: {},
},
},
include: {
posts: true,
},
})
过滤不存在 "-to-one" 关系的情况
以下查询返回所有没有作者关系的帖子
const postsWithNoAuthor = await prisma.post.findMany({
where: {
author: null, // or author: { }
},
include: {
author: true,
},
})
过滤存在相关记录的情况
以下查询返回所有至少有一篇帖子的用户
const usersWithSomePosts = await prisma.user.findMany({
where: {
posts: {
some: {},
},
},
include: {
posts: true,
},
})
流畅 API
流畅 API 让你通过函数调用流畅地遍历模型的关系。请注意,最后一个函数调用决定了整个查询的返回类型(相应的类型注解已在下面的代码片段中添加以明确说明)。
此查询返回特定 User
的所有 Post
记录
const postsByUser: Post[] = await prisma.user
.findUnique({ where: { email: 'alice@prisma.io' } })
.posts()
这等价于以下 findMany
查询
const postsByUser = await prisma.post.findMany({
where: {
author: {
email: 'alice@prisma.io',
},
},
})
这两个查询的主要区别在于,流畅 API 调用被转换为两个独立的数据库查询,而另一个只生成一个查询(参见这个 GitHub issue)
注意:你可以利用 Prisma Client 中 Prisma dataloader 自动对
.findUnique({ where: { email: 'alice@prisma.io' } }).posts()
查询进行批量处理的事实,以避免 GraphQL resolver 中的 n+1 问题。
此请求返回特定帖子的所有类别
const categoriesOfPost: Category[] = await prisma.post
.findUnique({ where: { id: 1 } })
.categories()
注意,你可以随意链式调用多个查询。在此示例中,链式调用从 Profile
开始,经过 User
到达 Post
const posts: Post[] = await prisma.profile
.findUnique({ where: { id: 1 } })
.user()
.posts()
链式调用的唯一要求是上一个函数调用必须只返回一个单个对象(例如,由 findUnique
查询返回的对象或像 profile.user()
这样的“to-one 关系”)。
以下查询不可能,因为 findMany
不返回单个对象,而是返回一个列表
// This query is illegal
const posts = await prisma.user.findMany().posts()