8. 实体和查询计划
5m

概述

是时候为我们的开发人员工具带添加一个新工具了:实体!

在本课中,我们将

  • 了解什么是,它的用途以及如何定义它
  • 了解 如何使用 表示和 从多个 连接数据
  • 了解 表示和引用 如何协同工作

食谱和配乐

还记得我们的梦想 吗?

梦想查询
query GetRecipeAndRecommendedSoundtracks {
randomRecipe {
id
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
tracks {
id
name
explicit
durationMs
}
}
}
}

我们设想生成一个随机食谱,并立即获得与之搭配的完美配乐推荐。我们称之为 recommendedPlaylists.

recommendedPlaylists 需要位于 soundtracks 中,因为它与音乐数据相关,但它属于 Recipe 类型。

最大的问题是什么? soundtracks 完全不知道 Recipe 类型是什么!让我们用实体来解决这个问题。

什么是实体?

一个 实体 是一个 ,其 分布在多个 中。它是联邦图架构的基本构建块,用于在子图之间连接数据,同时仍然遵守关注点分离原则。

一个 定义一个 可以执行以下一项或两项操作:

  • 贡献 不同的
  • 引用 一个 ,这意味着将其用作 中定义的另一个 的返回类型

贡献与引用

为了区分 贡献 到一个 ,以及那些 引用 一个 ,可以这样想:一个 贡献 实际上是将 它自己的域 中的新数据功能添加到 类型。

这与仅仅 引用 相反;它本质上只是“提及”实体的存在,并将其作为 另一个字段 的返回类型。

在联邦中,我们使用实体来创建一致的类型,这些类型不会局限于一个 或另一个;相反,它们可以跨越我们整个 API!

我们已经有了 在我们的 中。让我们转到 Studio,到我们的超级图页面,然后导航到 模式 页面。

从左侧列表中选择 对象。看到 Recipe 类型旁边的绿色 E 标签吗?E 代表

studio.apollographql.com

The Schema page, listing Object types with the Recipe entity highlighted

注意: 我们在 Explorer 的文档面板中看到了相同的标签,它位于返回 Recipe 类型的任何 旁边。

如何创建实体

要创建一个 ,一个 需要提供两项内容:一个 主键 和一个 引用解析器

定义主键

一个 主键 (或字段),它可以唯一地标识 中该 的一个实例。 使用主键从多个子图收集数据,并将其与单个实体实例相关联。这就是我们知道每个子图都在讨论 - 以及为其提供数据 - 的 相同 对象的方式!

例如,一个食谱的主键是它的id字段。这意味着,可以使用特定食谱的id从多个收集其数据。

Illustration showing three entities with unique ids

我们使用@key指令,以及一个名为fields的属性来设置我们想要用作主键的字段。

@key指令需要一个名为fields的属性,我们将其设置为我们想要用作主键的字段。

实体语法
type EntityType @key(fields: "id") {}

在 Hot Chocolate 中,这由[Key]属性表示。

定义引用解析函数

每个对贡献字段的也需要为该实体定义一个特殊的函数,称为引用解析器。引用负责返回的特定实例。

为了在 Hot Chocolate 中定义引用函数,我们使用属性[ReferenceResolver]

为了帮助返回的实例,引用将能够访问所谓的实体表示

什么是实体表示?

一个实体表示用来识别的特定实例的对象。表示始终包含该类型名对于该以及特定实例的@key字段。

  • __typename 字段:此自动存在于所有类型上。它始终以字符串形式返回其包含类型的名称。例如,Recipe.__typename返回“Recipe”。

  • @key 字段:键值对,可以使用它来识别的实例。例如,如果我们使用“id”字段定义Recipe实体作为主键,那么我们的实体表示将包含一个“id”属性,其值为“rec3j49yFpY2uRNM1”。

食谱的表示可能如下所示:

示例食谱实体表示
{
"__typename": "Recipe",
"id": "rec3j49yFpY2uRNM1"
}

您可以将表示视为需要将来自多个的数据关联起来,并确保每个子图都在讨论同一个对象的最小基本信息。

路由器如何使用实体和查询计划解析数据

让我们以我们梦寐以求的简化版本为例

query GetRecipeWithPlaylists {
randomRecipe {
name
description
recommendedPlaylists {
name
}
}
}

客户端将此发送到

步骤 1:构建查询计划

首先构建一个查询计划,指示要向哪些发送哪些请求。

从传入的顶级开始,即randomRecipe。在的帮助下,该看到randomRecipe是在recipes子图中定义的。

因此,该使用对recipes子图的请求开始查询计划。

继续执行此操作,针对检查中的每个字段,并将它们添加到查询计划中。该description字段也属于recipes子图。

但是当该到达特定ReciperecommendedPlaylists字段时,它从中看到Recipe.recommendedPlaylists只能由soundtracks子图解析(因为`Recipe.recommendedPlaylists 字段是在那里定义的)。

这意味着该将不得不连接来自不同的数据。

为此,该需要从recipes子图中获取更多信息:该Recipe对象的表示。

请记住, 表示是 用于在 之间跟踪特定对象的。要为 Recipe 对象创建实体表示, 需要食谱的类型名称及其主键(在本例中为 id )。

可以从 recipes 中获取这两个

从那里,该 在其 中添加另一个 ,以请求每个播放列表的 namesoundtracks 获取。

有了这个, 中的所有 都已在 中被考虑了。现在是时候进入下一步:执行该计划。

步骤 2:查询 recipes 子图

首先从 recipes 请求数据。

recipes 按正常方式解析所有请求的 ,包括所有请求的 Recipe 对象的 表示。

这个 并不知道 计划对食谱的 id 或类型名称进行任何特殊操作。它只是像被要求的那样将数据发回给路由器。

有了这个,该 就完成了 的第一步!下一步是检索 Playlist.name soundtracks 获取。

步骤 3:查询 soundtracks 子图

请记住我们在启用联合时出现在我们的 _entities 中的 ?这就是它重新回到故事中的地方!

使用 _entities 构建请求。

接受一个名为 representations,它接受一个 表示列表!这就是 recipes 收到的实体表示将要放置的地方。

在同一个请求中,该 添加了 中剩下的其他 (在本例中,每个播放列表的 name)。

将此请求发送到 soundtracks

为了解析 _entities ,该 soundtracks 使用其 引用解析器。请记住,这是一个特殊的 函数,用于返回该 贡献的所有

soundtracks 查看每个引用对象的 __typename 值,以确定 哪个 的引用 要使用。在本例中,因为类型名称是“食谱”,所以该 soundtracks 知道要使用 Recipe 的引用

Recipe 引用 中的每个 表示运行一次。每次它使用实体表示的主键来返回相应的 Recipe 对象。

在该 soundtracks 完成解析请求后,它将数据发回给

这就是执行阶段的全部内容!

步骤 4:将最终响应发送给客户端

现在,该 将从 recipessoundtracks 收到的所有数据组合成一个 JSON 对象。最后,该 将最终对象发回给客户端。

练习

实体的引用解析器函数应该在何处定义?
以下哪些步骤 **不** 作为路由器构建和执行其查询计划的一部分发生?

关键要点

  • 一个 是一个可以跨多个 解析其 的类型。
  • 要创建一个 ,我们可以使用 @key 来指定哪些 可以唯一标识该类型对象。
  • 我们可以用两种方式使用实体
    • 作为 的返回类型(引用 )。
    • 定义 用于来自多个 (为实体做出贡献)。
  • 任何 贡献 都需要为该实体定义一个引用 函数。这个 __resolveReference 会在 需要从另一个 中访问 时被调用。
  • 一个 表示是一个对象, 使用它来表示实体的特定实例。它包括实体的类型和它的键

接下来

让我们把这个理论付诸实践!我们将在 soundtracks 中为 Recipe 贡献

上一页

分享你关于本课的问题和意见

本课程目前处于

测试版
.您的反馈有助于我们改进!如果您遇到问题或感到困惑,请告诉我们,我们会帮助您解决。所有评论都是公开的,必须遵守 Apollo 行为准则。请注意,已解决或已解决的评论可能会被删除。

您需要一个 GitHub 帐户才能在下面发帖。没有帐户吗? 在我们的 Odyssey 论坛发帖。