概述
我们可以查询 播放列表的曲目,但只能通过 playlist(id: ID)
根 字段,而不是 通过 featuredPlaylists
。发生了什么事?
在本课中,我们将
- 了解 解析器 链
- 了解
parent
参数 的 解析器
检查数据源响应
让我们检查一下 GET /browse/featured-playlists
端点的响应。看起来我们确实可以访问 tracks
属性,它位于 playlist.items.tracks
下。
"tracks": {"href": "string","total": 0}
注意:或者,你可以在 SpotifyService
类中,按照类型和属性的轨迹找到相同的信息。
我们没有得到一个曲目对象的列表(类似于之前的 GET /playlists/{playlist_id}
端点返回的列表),而是得到一个具有两个属性的单个对象: total
,表示可用的总曲目数量,以及 href
,表示我们 可以 检索完整曲目对象列表的端点的 URL。
这是 REST API 中常见的模式。想象一下,如果响应 确实 包含了完整的曲目对象列表。为了包含播放列表的列表 以及 每个播放列表的曲目列表,这将导致非常大的响应。
相反,我们需要对 REST API 再进行一次额外的调用。在本例中,是 GET /playlists/{playlist_id}/tracks
端点。
下一个问题是:我们在代码中的哪个位置进行该调用?
检查查询
让我们退一步,看看我们想要实现的 查询:
query GetFeaturedPlaylists {featuredPlaylists {idnamedescriptiontracks {idnameexplicituri}}}
从这个 查询 中,我们使用 Query.FeaturedPlaylists
解析器 函数来解析 featuredPlaylists
字段:
public async Task<List<Playlist>> FeaturedPlaylists(SpotifyService spotifyService){var response = await spotifyService.GetFeaturedPlaylistsAsync();return response.Playlists.Items.Select(item => new Playlist(item)).ToList();}
当我们将响应转换为 Playlist
对象时,我们使用的是接受 PlaylistSimplified
类型的 Playlist
构造函数。
public Playlist(PlaylistSimplified obj){Id = obj.Id;Name = obj.Name;Description = obj.Description;}
在本例中,我们没有对播放列表的曲目进行任何初始化(与接受 SpotifyWeb.Playlist
对象的构造函数相比),这就是我们没有得到任何数据的原因!
请记住,我们需要对 GET /playlists/{playlist_id}/tracks
端点进行额外的调用才能获得曲目列表。我们回到了最初的问题:我们在代码中的哪个位置进行该调用?
我们 可以 在这个构造函数中添加它。这是我们在 解析器 函数语句中停下的地方。
public Playlist(PlaylistSimplified obj){Id = obj.Id;Name = obj.Name;Description = obj.Description;// should we make a call to spotifyService.GetPlaylistsTracksAsync() here?}
但这意味着,每当我们 查询 featuredPlaylists
时,我们都会 始终 对 REST API 进行额外的网络调用,即使 查询 没有 请求 tracks
!
所以,我们将使用 解析器 链。
沿着解析器链
一个 解析器链 是解析特定 GraphQL 操作 时,调用 解析器 函数的顺序。它可以包含顺序路径和并行分支。
让我们从我们的项目中举一个例子。这个 GetPlaylist
操作 检索播放列表的名称。
query GetPlaylist($playlistId: ID!) {playlist(id: $playlistId) {name}}
解析此 操作 时, GraphQL 服务器 将首先调用 Query.Playlist()
解析器 函数,然后调用 Playlist.Name()
函数,该函数返回 string
类型并结束链。
此链中的每个 解析器 都将它们的返回值传递给下一个函数,使用解析器的 parent
参数。
请记住,一个 解析器 可以访问多个参数。到目前为止,我们已经使用了 数据源 ( SpotifyService
)和 参数 (如播放列表 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 调用插入的最佳位置。
记住,我们一直在讨论是否将其包含在构造函数部分,在那里它将被 每次 调用,即使 操作 没有包含它:
public Playlist(PlaylistSimplified obj){Id = obj.Id;Name = obj.Name;Description = obj.Description;// should we make a call to spotifyService.GetPlaylistsTracksAsync() here?// probably not!}
相反,我们将重构 Playlist.Tracks
属性为一个带有函数体的 解析器 函数。现在,它是一个简单的 get;
属性 解析器。
首先,让我们为 tracks
添加一个私有 字段,在它前面加上一个下划线(_
)以遵循通用约定。我们将使它可为空。
private List<Track>? _tracks;
然后,我们将更新 Playlist(SpotifyWeb.Playlist obj)
构造函数以设置此私有 字段 的值。
- Tracks = obj.Tracks.Items.Select(item => new Track(item.Track)).ToList();+ _tracks = obj.Tracks.Items.Select(item => new Track(item.Track)).ToList();
接下来,我们将把 Tracks
属性转换为一个带有函数体的 解析器 函数,而不是 get; set;
方法。
- public List<Track> Tracks { get; set; }+ public List<Track> Tracks()+ {++ }
现在,我们将返回私有 _tracks
字段 中的内容。
return _tracks;
我们的 GetPlaylistDetails
操作 应该仍然可以使用这些更改。花点时间保存我们的更改并确认!
现在我们有一个完美的时机来进行我们的附加 HTTP 调用。
与其立即返回 _tracks
私有 字段,我们将检查它是否存在。如果存在,则返回它,但如果 不存在,我们将进行 HTTP 调用。
if (_tracks != null) {return _tracks;} else {// TODO: HTTP call}
由于这是一个常规的 解析器 函数,因此我们可以在函数参数中访问 SpotifyService
类。我们还将更新函数使其异步并返回 Task<List<Track>>
。
public async Task<List<Track>> Tracks(SpotifyService spotifyService)
接下来,在 else
块中,让我们调用服务的 GetPlaylistsTracksAsync
方法并 await
结果。
var response = await spotifyService.GetPlaylistsTracksAsync();
GetPlaylistsTracksAsync
方法需要一个 参数:播放列表的 ID。我们如何获取该值?
嗯,此方法属于 Playlist
类,因此我们可以访问 this.Id
。
var response = await spotifyService.GetPlaylistsTracksAsync(this.Id);
另一种访问播放列表 ID 的方法是通过 parent
参数在 解析器 函数中使用 [Parent]
属性。此属性使用依赖注入将 parent
的值注入到 解析器 中。
public async Task<List<Track>> Tracks(SpotifyService spotifyService,[Parent] Playlist parent)
在 解析器 函数体中,我们将替换 this.Id
为 parent.Id
。
// var response = await spotifyService.GetPlaylistsTracksAsync(this.Id); // same as belowvar response = await spotifyService.GetPlaylistsTracksAsync(parent.Id);
两种方法都有效!我们将坚持使用第一种方法,使用 this.Id
。
让我们完成我们的 解析器 函数。在调用 GetPlaylistsTracksAsync
后,我们将深入 response
的 Items
属性,遍历集合并从每个项目中创建一个 Track
对象。这应该很熟悉,我们已经在 Playlist (SpotifyWeb.Playlist obj)
构造函数中做了同样的事情!不要忘记在最后使用 ToList()
来匹配此 解析器 预期返回的类型。
return response.Items.Select(item => new Track(item.Track)).ToList();
探索时间:第二轮!
服务器正在运行最新的更改?很好!现在当我们跳回到沙盒并运行 查询 获取 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 问题,但它是在服务器端(响应和请求速度更加一致,通常更快),而不是在客户端(网络速度 可变 且不一致)。
注意:我们可以使用数据加载器在 GraphQL 端解决 N+1 问题。 查看 Hot Chocolate 文档以了解如何实现它们。
主要要点
- 一个 解析器 链是解析特定 GraphQL 操作 时调用解析器函数的顺序。它可以包含顺序路径以及并行分支。
- 此链中的每个 解析器 都将它们的返回值传递给下一个函数,使用解析器的
parent
参数。
下一步
对查询有信心了吗?现在是时候探索 GraphQL 的另一面:突变。
分享您关于本课程的问题和评论
本课程目前处于
您需要一个 GitHub 帐户才能在下面发布。还没有? 请在我们的 Odyssey 论坛中发布。