跳至主要内容

关联查询

Prisma Client 的一个关键功能是能够查询两个或多个模型之间的关系。关联查询包括

Prisma Client 还具有用于遍历关系的流畅 API

嵌套读取

嵌套读取允许您读取数据库中多个表中的相关数据,例如用户及其帖子。您可以

  • 使用include 在查询响应中包含相关记录,例如用户的帖子或个人资料。
  • 使用嵌套的select 包含来自相关记录的特定字段。您还可以将select嵌套在include中。

关系加载策略 (预览)

从版本5.8.0开始,您可以通过PostgreSQL 数据库的relationLoadStrategy选项,在每个查询级别决定如何希望 Prisma Client 执行关联查询(即应应用什么加载策略)。

从版本5.10.0开始,此功能也适用于 MySQL。

由于relationLoadStrategy选项目前处于预览阶段,因此您需要通过 Prisma 模式文件中的relationJoins预览功能标志启用它

schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["relationJoins"]
}

添加此标志后,您需要再次运行prisma generate以重新生成 Prisma Client。此功能目前在 PostgreSQL、CockroachDB 和 MySQL 上可用。

Prisma Client 支持两种关系加载策略

  • join(默认):使用数据库级别的LATERAL JOIN(PostgreSQL)或相关子查询(MySQL),并使用单个查询到数据库中获取所有数据。
  • query:向数据库发送多个查询(每个表一个),并在应用程序级别将它们连接起来。

这两个选项之间的另一个重要区别是,join策略在数据库级别使用 JSON 聚合。这意味着它在数据库中已经创建了 Prisma Client 返回的 JSON 结构,这可以节省应用程序级别的计算资源。

注意:一旦relationLoadStrategy预览阶段进入正式发布阶段,join将普遍成为所有关联查询的默认值。

示例

您可以在任何支持includeselect的查询的顶层使用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 JOIN和 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,
},
},
},
})
显示查询结果

您还可以将select嵌套在include中 - 以下示例返回所有User字段以及每个帖子的title字段

const user = await prisma.user.findFirst({
include: {
posts: {
select: {
title: true,
},
},
},
})
显示查询结果

请注意,您**不能**在同一级别使用selectinclude。这意味着,如果您选择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
}
}
},
})
显示CLI结果

而是,使用嵌套的select选项

const user = await prisma.user.findFirst({
select: {
// This will work!
email: true,
posts: {
select: {
title: true,
},
},
},
})

关系计数

3.0.1及更高版本中,您可以includeselect关系计数以及字段 - 例如,用户的帖子计数。

const relationCount = await prisma.user.findMany({
include: {
_count: {
select: { posts: true },
},
},
})
显示查询结果

过滤关系列表

当您使用selectinclude返回相关数据的子集时,您可以在selectinclude内部过滤和排序关系列表

例如,以下查询返回所有用户以及与每个用户关联的未发布帖子的标题列表

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: '[email protected]',
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
},
})
显示查询结果

创建或更新单个记录和多个关联记录(例如,一个用户有多个帖子)有两种方法。

在大多数情况下,除非需要 skipDuplicates 查询选项,否则嵌套的 create 会更可取。以下是一个快速表格,描述了这两个选项之间的差异。

特性createcreateMany说明
支持嵌套其他关联✘ *例如,您可以在一个查询中创建一个用户、几个帖子以及每个帖子的几个评论。
* 您可以手动在一对一关系中设置外键,例如:{ authorId: 9}
支持 1-n 关系例如,您可以创建一个用户和多个帖子(一个用户有多个帖子)。
支持 m-n 关系例如,您可以创建一个帖子和几个类别(一个帖子可以有多个类别,一个类别可以有多个帖子)。
支持跳过重复记录使用 skipDuplicates 查询选项。

使用嵌套 create

以下查询使用嵌套的 create 来创建

  • 一个用户
  • 两个帖子
  • 一个帖子类别

此示例还使用嵌套的 include 将所有帖子和帖子类别包含在返回的数据中。

const result = await prisma.user.create({
data: {
email: '[email protected]',
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
},
},
},
})
显示查询结果

以下是一个可视化表示,说明嵌套的 create 操作如何一次写入数据库中的多个表。

使用嵌套 createMany

以下查询使用嵌套的 createMany 来创建

  • 一个用户
  • 两个帖子

此示例还使用嵌套的 include 将所有帖子包含在返回的数据中。

const result = await prisma.user.create({
data: {
email: '[email protected]',
posts: {
createMany: {
data: [{ title: 'My first post' }, { title: 'My second post' }],
},
},
},
include: {
posts: true,
},
})
显示查询结果
警告

注意:在突出显示的查询中**无法**嵌套额外的 createcreateMany,这意味着您不能同时创建用户、帖子和帖子类别。

您无法在 createMany()createManyAndReturn() 查询中访问关联,这意味着您无法在一个嵌套写入中创建多个用户和多个帖子。以下操作是**不可行**的。

const createMany = await prisma.user.createMany({
data: [
{
name: 'Yewande',
email: '[email protected]',
posts: {
// Not possible to create posts!
},
},
{
name: 'Noor',
email: '[email protected]',
posts: {
// Not possible to create posts!
},
},
],
})

连接多个记录

以下查询创建 (create ) 一个新的 User 记录,并将该记录 (connect ) 连接到三个现有的帖子。

const result = await prisma.user.create({
data: {
email: '[email protected]',
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 连接相关记录。

const result = await prisma.post.create({
data: {
title: 'How to make croissants',
author: {
connectOrCreate: {
where: {
email: '[email protected]',
},
create: {
email: '[email protected]',
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 一对多关系中*所有*关联记录(一个用户有多个帖子),请将关系设置为空列表,如所示。

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 更新 "[email protected]"(如果该用户存在),或者如果该用户不存在则创建该用户。

const result = await prisma.post.update({
where: {
id: 6,
},
data: {
author: {
upsert: {
create: {
email: '[email protected]',
name: 'Bob the New User',
},
update: {
email: '[email protected]',
name: 'Bob the existing user',
},
},
},
},
include: {
author: true,
},
})

您可以在 update 中嵌套 createcreateMany,以向现有记录添加新的关联记录。以下查询向 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 提供了 someeverynone 选项,用于根据关系“-to-many”一侧关联记录的属性过滤记录。例如,根据帖子的属性过滤用户。

例如

需求要使用的查询选项
"我想要一个包含所有具有*至少一个*未发布 Post 记录的 User 的列表"某些帖子未发布
"我想要一个包含所有具有*没有*未发布 Post 记录的 User 的列表"没有帖子未发布
"我想要一个包含所有只有未发布 Post 记录的 User 的列表"每个帖子都未发布

例如,以下查询返回符合以下条件的 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 提供了 isisNot 选项,用于根据关系“-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,
},
})

Fluent API

Fluent API 允许您通过函数调用*流畅地*遍历模型的 关联。请注意,*最后一个*函数调用决定了整个查询的返回类型(以下代码片段中添加了相应的类型注释以明确说明这一点)。

此查询返回特定 User 的所有 Post 记录。

const postsByUser: Post[] = await prisma.user
.findUnique({ where: { email: '[email protected]' } })
.posts()

这等效于以下 findMany 查询。

const postsByUser = await prisma.post.findMany({
where: {
author: {
email: '[email protected]',
},
},
})

这两个查询的主要区别在于,Fluent API 调用被翻译成两个独立的数据库查询,而另一个查询只生成一个查询(参见此 GitHub 问题)。

注意:您可以利用 Prisma Client 中的 Prisma 数据加载器自动批处理 .findUnique({ where: { email: '[email protected]' } }).posts() 查询的事实来 避免 GraphQL 解析器中的 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()返回)。

以下查询**无法执行**,因为findMany返回的不是单个对象,而是一个列表

// This query is illegal
const posts = await prisma.user.findMany().posts()