13. 解析器链
5m

概述

我们可以 播放列表的曲目,但只能通过 playlist(id: ID)不是 通过 featuredPlaylists. 发生了什么事?

在本课中,我们将

  • 了解
  • 了解 parent

检查数据源响应

让我们检查来自我们 GET /browse/featured-playlists 终端的响应。看起来我们确实可以访问 tracks 属性,位于 playlist.items.tracks 下。

"tracks": {
"href": "string",
"total": 0
}

注意:或者,你可以通过在 mock_spotify_rest_api_client 包中遵循类型和属性的轨迹来找到相同的信息。

我们没有获得曲目对象的列表(类似于较早的 GET /playlists/{playlist_id} 终端返回的列表),而是获得一个具有两个属性的单个对象: total,可用的曲目总数,以及 href,一个指向终端的 URL,我们 可以 检索完整曲目对象列表。

这是 REST API 中常见的模式。想象一下,如果响应 确实 包含完整的曲目对象列表。这将导致非常大的响应,因为要有一个播放列表列表 每个播放列表的曲目列表。

这意味着我们无法在 Query.featured_playlists 函数中获得播放列表的曲目列表。

相反,我们需要再向 REST API 发起一次额外的调用。在这种情况下,是 GET /playlists/{playlist_id}/tracks 终端。

下一个问题是:我们在代码中的哪个位置发起该调用?

检查查询

让我们后退一步,看看我们想要实现的

query GetFeaturedPlaylists {
featuredPlaylists {
id
name
description
tracks {
id
name
explicit
uri
}
}
}

从这个 中,我们使用 Query.featured_playlists 函数解析 featuredPlaylists

api/query.py
async def featured_playlists(self, info: strawberry.Info) -> list[Playlist]:
client = info.context["spotify_client"]
data = await get_featured_playlists.asyncio(client=client)
return [
Playlist(
id=strawberry.ID(playlist.id),
name=playlist.name,
description=playlist.description,
)
for playlist in data.playlists.items
]

在这种情况下,我们没有做任何事情来初始化播放列表的曲目,这就是我们收到错误的原因(tracks 是必需的,但未提供)。

请记住,我们需要向 GET /playlists/{playlist_id}/tracks 终端发起额外的调用来获取曲目列表。我们又回到了最初的问题:我们在代码中的哪个位置发起该调用?

我们 可以 在同一个 featured_playlists 函数中添加它。

但这意味着,每当我们 featuredPlaylists 时,我们都会 始终 向 REST API 发起额外的网络调用,即使 没有 请求 tracks

因此,我们将使用 链。

沿着解析器链前进

一个 解析器链 是解析特定 时调用 函数的顺序。它可以包含顺序路径以及并行分支。

让我们从我们的项目中举个例子。这个 GetPlaylist 检索播放列表的名称。

query GetPlaylist($playlistId: ID!) {
playlist(id: $playlistId) {
name
}
}

解析此 时, 将首先调用 Query.playlist 函数,然后调用 playlist.name 函数,它返回 str 类型并结束链。

Resolver chain in a diagram

此链中的每个 都使用解析器的 parent 将其返回值传递给下一个函数。在 Python 中,我们使用 self 访问此 parent 对象。

请记住,一个 可以访问多个参数。到目前为止,我们已经使用了 info (从 context 获取 spotify_client)和 (例如播放列表 id )。parent 是另一个这样的参数!

在本例中,playlist.name 函数将可以访问 Query.playlist 返回的 Playlist 对象。

让我们来看另一个

query GetPlaylistTracks($playlistId: ID!) {
playlist(id: $playlistId) {
name
tracks {
uri
}
}
}

这次,我们添加了更多 ,并请求了每个播放列表的曲目列表,特别是它们的 uri 值。

我们的 链增长了,添加了一个并行分支。

Resolver chain in a diagram

请注意,由于 Playlist.tracks 返回一个可能包含多个曲目的列表,因此此 可能需要运行多次才能检索每个曲目的 URI。

沿着 的轨迹,Playlist.tracks 将可以访问 Playlist 作为 父级Track.uri 将可以访问 Track 对象作为 父级

如果我们的 没有包含 tracks (就像我们展示的第一个示例一样),那么 Playlist.tracks 永远不会被调用!

重构 Playlist.tracks

现在我们已经了解了 链是什么,我们可以用它来确定将额外的 REST API 调用插入到播放列表的曲目的最佳位置。

请记住,我们正在讨论是否将其包含在 featured_playlists 中,在那里它将 每次 被调用,即使 没有包含它。

相反,我们将跳入 Playlist 类并重构 tracks 属性为一个具有主体部分的 函数。现在,它是一个基本属性,具有一个默认解析器。

api/types/playlist.py
tracks: list[Track] = strawberry.field(description="The playlist's tracks.")

首先,让我们为 tracks 添加一个私有 ,用下划线 (_) 作为前缀以遵循通用约定。为了防止此 出现在模式中,我们使用 strawberry.Private 函数。

我们还将为它指定一个默认值 None,这样我们就不必在构造函数中传递它,因为我们还没有数据。

api/types/playlist.py
_tracks: strawberry.Private[list[Track] | None] = None

然后,我们将更新 Query.playlist,以便在创建 Playlist 对象时设置 _tracks (而不是没有下划线的 tracks):

api/query.py
return Playlist(
id=strawberry.ID(data.id),
name=data.name,
description=data.description,
- tracks=[
+ _tracks=[
Track(
id=strawberry.ID(item.track.id),
name=item.track.name,
duration_ms=item.track.duration_ms,
explicit=item.track.explicit,
uri=item.track.uri,
)
for item in data.tracks.items
],
)

回到 Playlist 类,让我们将 tracks 属性转换为一个 函数并应用 @strawberry.field 装饰器。

api/types/playlist.py
- tracks: list[Track] = strawberry.field(description="The tracks in the playlist.")
+ @strawberry.field(description="The tracks in the playlist.")
+ def tracks(self) -> list[Track]:

现在,我们将返回私有 _tracks 中的内容。

api/types/playlist.py
return self._tracks

我们的 GetPlaylistDetails 应该仍然可以使用这些更改。花点时间保存我们的更改并确认!

现在我们有一个完美的时机来进行额外的 HTTP 调用。

与其立即返回 _tracks 私有 ,我们将检查它是否存在。如果存在,则返回它,但如果 不存在,我们将进行 HTTP 调用。

api/types/playlist.py
def tracks(self) -> list[Track]:
if self._tracks is None:
# TODO: HTTP call
...
return self._tracks

现在进行 HTTP 调用!首先,我们需要访问 info ,然后从 info.context 中提取 spotify_client。我们还将更新该函数使其为 async

api/types/playlist.py
async def tracks(
self,
info: strawberry.Info
) -> list[Track]:
if self._tracks is None:
spotify_client = info.context["spotify_client"]
# TODO: HTTP call
return self._tracks

接下来,让我们调用 get_playlist_tracks 函数。我们将传递 spotify_client 和播放列表的 idawait 结果并将其存储在一个名为 data 中。您应该对此已经很熟悉了!

api/types/playlist.py
if self._tracks is None:
spotify_client = info.context["spotify_client"]
data = await get_playlists_tracks.asyncio(
client=spotify_client, playlist_id=self.id
)

然后,我们将使用从 REST API 调用返回的数据更新 _tracks ,使用必要的属性实例化一个 Track 类列表。

api/types/playlist.py
data = await get_playlists_tracks.asyncio(
client=spotify_client, playlist_id=self.id
)
self._tracks = [
Track(
id=strawberry.ID(item.track.id),
name=item.track.name,
duration_ms=item.track.duration_ms,
explicit=item.track.explicit,
uri=item.track.uri,
)
for item in data.items
]

最后,不要忘记在文件顶部导入 get_playlists_tracks 函数。

api/types/playlist.py
from mock_spotify_rest_api_client.api.playlists import get_playlists_tracks

探索时间:第二轮!

服务器正在运行最新更改了吗?太好了!现在,当我们回到 Sandbox 并运行 以获取 featuredPlaylists 及其曲目列表时,我们得到了我们所请求的!

query GetFeaturedPlaylists {
featuredPlaylists {
id
name
description
tracks {
id
name
explicit
uri
}
}
}

👏👏👏

与 REST 方法比较

现在让我们再次戴上产品应用程序开发人员的帽子!让我们比较一下,如果我们使用 REST 而不是 ,此功能将是什么样子。

如果我们使用 REST,应用程序逻辑将包括

  • /browse/featured-playlists 端点进行 HTTP GET 调用
  • 对响应中 每个播放列表 进行额外的 HTTP GET 调用 GET /playlists/{playlist_id}/tracks。等待所有这些解析,这取决于播放列表的数量,可能需要一段时间。此外,这引入了 常见的 N+1 问题
  • 仅检索 idnameexplicit 以及 uri 属性,丢弃 所有其他 响应。响应中还有许多其他未使用的数据!同样,如果客户端应用程序的网络速度很慢或者没有太多数据,那么如此大的响应将带来成本。

使用 ,我们从客户端获得简洁、干净、易读的 ,并以他们指定的精确形式返回,不多不少!

所有提取数据、进行额外的 HTTP 调用和过滤哪些 的逻辑都在 端完成。我们仍然存在 N+1 问题,但它是在服务器端(响应和请求速度更加一致,通常更快)而不是客户端(网络速度 不一致)发生的。

注意:我们可以使用 DataLoaders 解决 方面的 N+1 问题。 查看 Strawberry 文档以了解如何实现它们

关键要点

  • 一个 链是解析特定 时调用解析器函数的顺序。它可以包含顺序路径以及并行分支。
  • 此链中的每个 都使用解析器的 parent 将其返回值传递给下一个函数。

接下来

对查询感到自信了吗?现在是探索 的另一面:

上一个

分享您关于本课的疑问和评论

本课程目前处于

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

您需要一个 GitHub 帐户才能在下方发帖。没有帐户吗? 请改为在我们 Odyssey 论坛中发帖。