概述
我们可以查询 播放列表的曲目,但只能通过 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 {idnamedescriptiontracks {idnameexplicituri}}}
从这个 查询 中,我们使用 Query.featured_playlists
解析器 函数解析 featuredPlaylists
字段:
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
!
因此,我们将使用 解析器 链。
沿着解析器链前进
一个 解析器链 是解析特定 GraphQL 操作 时调用 解析器 函数的顺序。它可以包含顺序路径以及并行分支。
让我们从我们的项目中举个例子。这个 GetPlaylist
操作 检索播放列表的名称。
query GetPlaylist($playlistId: ID!) {playlist(id: $playlistId) {name}}
解析此 操作 时, GraphQL 服务器 将首先调用 Query.playlist
解析器 函数,然后调用 playlist.name
函数,它返回 str
类型并结束链。
此链中的每个 解析器 都使用解析器的 parent
参数 将其返回值传递给下一个函数。在 Python 中,我们使用 self
访问此 parent
对象。
请记住,一个 解析器 可以访问多个参数。到目前为止,我们已经使用了 info
(从 context
获取 spotify_client
)和 参数 (例如播放列表 id
参数)。parent
是另一个这样的参数!
在本例中,playlist.name
的 解析器 函数将可以访问 Query.playlist
返回的 Playlist
对象。
让我们来看另一个 GraphQL 操作。
query GetPlaylistTracks($playlistId: ID!) {playlist(id: $playlistId) {nametracks {uri}}}
这次,我们添加了更多 字段,并请求了每个播放列表的曲目列表,特别是它们的 uri
值。
我们的 解析器 链增长了,添加了一个并行分支。
请注意,由于 Playlist.tracks
返回一个可能包含多个曲目的列表,因此此 解析器 可能需要运行多次才能检索每个曲目的 URI。
沿着 解析器 的轨迹,Playlist.tracks
将可以访问 Playlist
作为 父级,
Track.uri
将可以访问 Track
对象作为 父级。
如果我们的 操作 没有包含 tracks
字段(就像我们展示的第一个示例一样),那么 Playlist.tracks
的 解析器 永远不会被调用!
重构 Playlist.tracks
现在我们已经了解了 解析器 链是什么,我们可以用它来确定将额外的 REST API 调用插入到播放列表的曲目的最佳位置。
请记住,我们正在讨论是否将其包含在 featured_playlists
的 解析器 中,在那里它将 每次 被调用,即使 操作 没有包含它。
相反,我们将跳入 Playlist
类并重构 tracks
属性为一个具有主体部分的 解析器 函数。现在,它是一个基本属性,具有一个默认解析器。
tracks: list[Track] = strawberry.field(description="The playlist's tracks.")
首先,让我们为 tracks
添加一个私有 字段,用下划线 (_
) 作为前缀以遵循通用约定。为了防止此 字段 出现在模式中,我们使用 strawberry.Private
函数。
我们还将为它指定一个默认值 None
,这样我们就不必在构造函数中传递它,因为我们还没有数据。
_tracks: strawberry.Private[list[Track] | None] = None
然后,我们将更新 Query.playlist
的 解析器,以便在创建 Playlist
对象时设置 _tracks
字段(而不是没有下划线的 tracks
):
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
装饰器。
- 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
字段 中的内容。
return self._tracks
我们的 GetPlaylistDetails
操作 应该仍然可以使用这些更改。花点时间保存我们的更改并确认!
现在我们有一个完美的时机来进行额外的 HTTP 调用。
与其立即返回 _tracks
私有 字段,我们将检查它是否存在。如果存在,则返回它,但如果 不存在,我们将进行 HTTP 调用。
def tracks(self) -> list[Track]:if self._tracks is None:# TODO: HTTP call...return self._tracks
现在进行 HTTP 调用!首先,我们需要访问 info
参数,然后从 info.context
中提取 spotify_client
。我们还将更新该函数使其为 async
:
async def tracks(self,info: strawberry.Info) -> list[Track]:if self._tracks is None:spotify_client = info.context["spotify_client"]# TODO: HTTP callreturn self._tracks
接下来,让我们调用 get_playlist_tracks
函数。我们将传递 spotify_client
和播放列表的 id
,await 结果并将其存储在一个名为 变量 的
data 中。您应该对此已经很熟悉了!
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
类列表。
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
函数。
from mock_spotify_rest_api_client.api.playlists import get_playlists_tracks
探索时间:第二轮!
服务器正在运行最新更改了吗?太好了!现在,当我们回到 Sandbox 并运行 查询 以获取 featuredPlaylists
及其曲目列表时,我们得到了我们所请求的!
query GetFeaturedPlaylists {featuredPlaylists {idnamedescriptiontracks {idnameexplicituri}}}
👏👏👏
与 REST 方法比较
现在让我们再次戴上产品应用程序开发人员的帽子!让我们比较一下,如果我们使用 REST 而不是 GraphQL,此功能将是什么样子。
如果我们使用 REST,应用程序逻辑将包括
- 对
/browse/featured-playlists
端点进行 HTTP GET 调用 - 对响应中 每个播放列表 进行额外的 HTTP GET 调用
GET /playlists/{playlist_id}/tracks
。等待所有这些解析,这取决于播放列表的数量,可能需要一段时间。此外,这引入了 常见的 N+1 问题。 - 仅检索
id
、name
和explicit
以及uri
属性,丢弃 所有其他 响应。响应中还有许多其他未使用的数据!同样,如果客户端应用程序的网络速度很慢或者没有太多数据,那么如此大的响应将带来成本。
使用 GraphQL,我们从客户端获得简洁、干净、易读的 操作,并以他们指定的精确形式返回,不多不少!
所有提取数据、进行额外的 HTTP 调用和过滤哪些 字段 的逻辑都在 GraphQL 服务器 端完成。我们仍然存在 N+1 问题,但它是在服务器端(响应和请求速度更加一致,通常更快)而不是客户端(网络速度 变化 不一致)发生的。
注意:我们可以使用 DataLoaders 解决 GraphQL 方面的 N+1 问题。 查看 Strawberry 文档以了解如何实现它们。
关键要点
- 一个 解析器 链是解析特定 GraphQL 操作 时调用解析器函数的顺序。它可以包含顺序路径以及并行分支。
- 此链中的每个 解析器 都使用解析器的
parent
参数 将其返回值传递给下一个函数。
接下来
对查询感到自信了吗?现在是探索 GraphQL 的另一面: 变异。
分享您关于本课的疑问和评论
本课程目前处于
您需要一个 GitHub 帐户才能在下方发帖。没有帐户吗? 请改为在我们 Odyssey 论坛中发帖。