概述
我们可以查询 房源的便利设施,但只能通过 listing(id: ID)
根 字段,并非 通过 featuredListings
。发生了什么?
在本课中,我们将
- 了解 解析器 链
- 了解
source
参数 在数据提取器方法中的作用 - 将本地上下文从一个数据提取器方法传递到下一个
检查数据源响应
让我们再次检查来自我们 GET /featured-listings
端点的响应。打开一个新的浏览器标签页,然后粘贴下面的 URL。
https://rt-airlock-services-listing.herokuapp.com/featured-listings
我们返回的数组包含我们期望的房源对象;但我们会注意到每个房源对象中有一个属性不同!从 /featured-listings
端点,房源的 "amenities"
列表是一个数组,其中包含 仅 每个便利设施的标识符。没有其他数据!
这是 REST API 中的常见模式。想象一下,如果每个房源的便利设施列表包含 完整 的每个便利设施的数据。这将导致非常大的响应,因为要返回房源列表 以及 每个房源的完整便利设施列表!如果端点决定返回比我们已经拥有的三个房源更多的房源,问题将进一步加剧。
为了确保我们可以返回房源及其 所有 便利设施的完整数据,我们需要对 REST API 进行一次额外的调用。在这种情况下,要调用一个新的端点:GET /listings/{listing_id}/amenities
。
下一个问题是:我们在代码的哪个地方进行此调用?
处理缺少的 amenities
为了处理我们缺少的 amenities
,我们 可以 更新我们的 featuredListings
数据提取器方法。我们可以为其添加一些额外的逻辑来访问 GET /listings/{listing_id}
以获取这些额外数据。
@DgsQuerypublic List<ListingModel> featuredListings() throws IOException {List<ListingModel> listings = listingService.featuredListingsRequest();// traverse listings for each listing id?// make a follow-up request to /listings/{listing_id}/amenities// then recompose each ListingModel object with the new amenities data?return listings;}
嗯,这可以实现;但意味着 每次 我们 查询 featuredListings
时,我们都会 始终 对 REST API 进行额外的网络调用,无论 查询 是否要求房源的 amenities
。
所以,我们将使用 解析器 链。
遵循解析器链
一个 解析器链 是数据提取器方法(在某些其他框架中称为 解析器 函数)在解析特定 GraphQL 操作 时调用的顺序。它可以包含顺序路径和并行分支。
让我们从我们的项目中举一个例子。这个 GetListing
操作 检索房源的标题。
query GetListing($listingId: ID!) {listing(id: $listingId) {title}}
在解析此 操作 时,GraphQL 服务器 将首先调用 Query.listing()
数据提取器方法,该方法返回一个 Listing
类型,然后调用 Listing.title()
方法,该方法返回一个 String
类型并结束链。
注意: 我们不需要为 单独 的数据提取器方法定义 Listing.title
,因为 title
属性可以直接从 Query.listing
返回的实例中返回。
此链中的每个数据提取器方法都会将其返回值作为 DgsDataFetchingEnvironment
上的一个属性传递给下一个方法。
该 DgsDataFetchingEnvironment
参数 是数据提取器方法可选使用的,但它包含有关正在执行的 查询、服务器上下文以及我们关心的参数的很多信息:source
。
在此示例中,Listing.title()
数据提取器方法可以使用 DgsDataFetchingEnvironment
的 source
属性访问 Listing
对象,该对象是 Query.listing()
方法返回的。
让我们看看另一个 GraphQL 操作。
query GetListingAmenities($listingId: ID!) {listing(id: $listingId) {titleamenities {name}}}
这次,我们添加了更多 字段 并请求了每个房源的便利设施列表,特别是它们的 name
值。
我们的 解析器 链正在增长,并添加了一个并行分支。
因为 Listing.amenities
返回一个可能包含多个便利设施的列表,所以此 解析器 可能运行多次以检索每个便利设施的名称。
沿着 解析器 的轨迹,Listing.amenities()
将可以访问 Listing
作为 source
,就像 Amenity.name()
可以访问 Amenity
对象作为 source
一样。
如果我们的 操作 没有包含 amenities
字段(就像我们展示的第一个示例一样),那么 Listing.amenities()
方法将永远不会被调用!
Listing.amenities
数据提取器方法
现在我们已经了解了 解析器 链,我们可以使用它来确定插入房源便利设施的额外 REST API 调用的最佳位置。
请记住,我们正在讨论是否要将其包含在 Query.featuredListings
数据提取器方法中,这样每次我们 调用 查询 特色列表数据时,即使操作不包含 amenities
字段,它也会被调用:
@DgsQuerypublic List<ListingModel> featuredListings() throws IOException {// Not the best place to make an extra network call!return listingService.featuredListingsRequest();}
相反,我们将添加一个 新的 数据提取器方法到 ListingDataFetcher
类中 - 一个专门负责满足 Listing.amenities
字段 来自我们模式的字段。
对于我们最后两个数据提取器方法,我们使用了 @DgsQuery
注释。这是因为我们定义的方法负责为我们模式中的 Query
类型上的 字段 提供数据。
但是,这次我们想定义一个方法,负责为 Listing
类型上的 字段 填充数据。为此,我们需要使用不同的注释: @DgsData
。
type Listing {id: ID!# ... other Listing fields"The amenities available for this listing"amenities: [Amenity!]! # We want to define a datafetcher method for THIS field!}
The @DgsData
注释
The @DgsData
注释允许我们指定我们为其定义方法的 GraphQL 类型和 字段。对于 Listing.amenities
字段,它将如下所示:
@DgsData(parentType="Listing", field="amenities")
如果我们给我们的方法与 字段 同样的名称,amenities
,我们可以在注释中省略 field="amenities"
规范。
现在,让我们在我们的 ListingDataFetcher
类中定义此方法。
@DgsData(parentType="Listing")public void amenities() {}
我们将在顶部导入新的 @DgsData
注释,以及我们服务器生成的 Amenity
类型,我们将在稍后使用它。
import com.netflix.graphql.dgs.DgsData;import com.example.listings.generated.types.Amenity;
Returning Listing.amenities
立即,我们可以将我们方法的返回类型更新为 List
Amenity
类型。
@DgsData(parentType="Listing")public List<Amenity> amenities() {// TODO}
现在,是时候利用那个 source
参数 了,我们在本课的前面提到了它。请记住,source
是我们为其解析便利设施的列表实例;它是 先前 数据提取器方法返回的值。
我们还没有为 Listing
类型的 id
、title
等定义单独的数据提取器方法,因此我们的服务器将在每个 ListingModel
实例上查找这些属性,这些实例是我们 查询 列表或特色列表时返回的。但是,我们的 Listing.amenities
字段 现在拥有自己的数据提取器方法。我们的服务器不会仅仅在 ListingModel
实例中检查其 amenities
属性,它将依赖于 此 方法来最终决定为列表的便利设施返回哪些数据。
为了简化工作,此方法将它正在为其解析便利设施的 ListingModel
作为 source
属性接收 DgsDataFetchingEnvironment
。这使我们能够访问和使用 ListingModel
属性(例如其 id
)来使我们的后续请求成为可能。
让我们在文件顶部导入 DgsDataFetchingEnvironment
。
// ... other importsimport com.netflix.graphql.dgs.DgsDataFetchingEnvironment;
接下来,我们将它作为 参数 添加到我们称为 dfe
的方法中。
@DgsData(parentType="Listing")public List<Amenity> amenities(DgsDataFetchingEnvironment dfe) {// TODO}
要访问 source
属性,我们将调用 dfe.getSource()
。我们将以 ListingModel
类型接收此值,称为 listing
。
public List<Amenity> amenities(DgsDataFetchingEnvironment dfe) {ListingModel listing = dfe.getSource();}
接下来,我们将访问 listing
中的 id
属性。
public List<Amenity> amenities(DgsDataFetchingEnvironment dfe) {ListingModel listing = dfe.getSource();String id = listing.getId();}
那么,我们如何判断我们正在解析的 ListingModel
实例是否 已经 附加了所有便利设施数据?我们知道,当我们 查询 单个列表时,我们从 REST API 获取所有便利设施数据;相反,当我们查询 featuredListings
时,我们只获得每个列表的便利设施 id
列表;name
和 category
为空!它们没有包含在 REST API 响应中。
// Amenities on an instance of ListingModel when returned by the Query.listing datafetcherListing{id='listing-1',amenities='[Amenity{id='am-2',category='Accommodation Details',name='Towel'},Amenity{id='am-10',category='Space Survival',name='Oxygen'},Amenity{id='am-11',category='Space Survival',name='Prepackaged meals'}]'}// Amenities on an instance of ListingModel when returned by the Query.featuredListings datafetcherListing{id='listing-1',amenities='[Amenity{id='am-2',category='null',name='null'},Amenity{id='am-10',category='null',name='null'},Amenity{id='am-11',category='null',name='null'}]}
DGS 中的本地上下文
DGS 为我们提供了一种在数据提取器之间传递自定义上下文的方法:使用 DataFetcherResult
类型。我们将使用此类型的 localContext
属性来帮助我们确定我们是否正在为 ListingModel
解析便利设施,该 已经 在类上设置了便利设施,或者我们是否需要进行后续请求。
首先,让我们在文件顶部导入 DataFetcherResult
类型,以及 Java 的 Map
实用程序。
import graphql.execution.DataFetcherResult;import java.util.Map;
然后,我们将 listing
方法的返回类型用 DataFetcherResult
包装起来。
@DgsQuerypublic DataFetcherResult<ListingModel> listing(@InputArgument String id) {return listingService.listingRequest(id);}
现在,我们将在代码中看到一个错误:listing
方法不再返回我们指示的类型。我们不会直接返回数据,而是将其捕获在 变量 ListingModel
中,称为 listing
。
@DgsQuerypublic DataFetcherResult<ListingModel> listing(@InputArgument String id) {ListingModel listing = listingService.listingRequest(id);}
要返回 DataFetcherResult
类型,我们需要构建该类型的实例,并附加其他属性。语法将类似于以下内容:
return DataFetcherResult.<T>newResult().data() // pass in the data to return.localContext() // attach local context.build();
让我们使用此语法更新我们的方法。对于 T
的值,即 变量,我们将给出我们方法的原始返回类型,ListingModel
。我们将 listing
变量 传递到 data()
中。
return DataFetcherResult.<ListingModel>newResult().data(listing).localContext().build();
The localContext
属性是我们将在此数据提取器方法中使用的属性,以指示链中的 下一个 数据提取器,我们返回的 ListingModel
已经 拥有其完整的便利设施数据。
为此,我们将传入一个新的 Map
,其中包含一个属性 hasAmenityData
,我们将其设置为 true
。
return DataFetcherResult.<ListingModel>newResult().data(listing).localContext(Map.of("hasAmenityData", true)).build();
现在,让我们对 featuredListings
做同样的事情。语法将与之前相同,除了 localContext
的值,我们将在其中将 hasAmenityData
属性设置为 false
。
@DgsQuerypublic DataFetcherResult<List<ListingModel>> featuredListings() throws IOException {List<ListingModel> listings = listingService.featuredListingsRequest();return DataFetcherResult.<List<ListingModel>>newResult().data(listings).localContext(Map.of("hasAmenityData", false)).build();}
使用 getLocalContext
检索上下文
我们的 Query.listing
和 Query.featuredListings
数据提取器方法都设置了一些自定义上下文;现在,让我们设置允许我们 检索 来自另一个数据提取器方法的上下文的语法。
滚动回到 Listing.amenities
数据提取器方法。
我们将再次使用 dfe
参数,这次我们将调用 getLocalContext
。我们将以 Map<String, Boolean>
类型接收它,我们将称其为 localContext
。
Map<String, Boolean> localContext = dfe.getLocalContext();
我们将检查 localContext
是否存在,以及 hasAmenityData
在 localContext
上是否为 true
,如果是,我们可以安全地假设我们的 ListingModel
实例已经设置了其 amenities
属性并完全填充。
Map<String, Boolean> localContext = dfe.getLocalContext();if (localContext != null && localContext.get("hasAmenityData")) {return listing.getAmenities();}
现在,如果我们的本地上下文 没有 设置 hasAmenityData
为 true
,那么我们将假设需要进行后续请求以获取完整的便利设施数据。
Map<String, Boolean> localContext = dfe.getLocalContext();if (localContext != null && localContext.get("hasAmenityData")) {return listing.getAmenities();}// TODO: FOLLOW-UP REQUEST HERE
让我们在 ListingService
中构建可以请求便利设施数据的调用。
请求便利设施
现在让我们跳到我们的 datasources/ListingService
文件并构建此方法。首先,从我们的 generated
文件夹导入 Amenity
类型。
// ... other importsimport com.example.listings.generated.types.Amenity;
我们需要此方法返回一个 List
的 Amenity
类型,因为这是我们的 Listing.amenities
字段 所期望返回的。它将接收我们想要检索其便利设施的列表的 id
,一个 String
我们称之为 listingId
。
public List<Amenity> amenitiesRequest(String listingId) {// TODO}
我们将使用之前使用的相同样板启动此请求:在我们的类 client
实例上调用 get
,并连接 uri
。我们正在访问 /listings/{listing_id}/amenities
端点,将 listingId
作为 {listing_id}
的值传递。
client.get().uri("/listings/{listing_id}/amenities", listingId)
接下来,我们将连接 retrieve
和 body
。
client.get().uri("/listings/{listing_id}/amenities", listingId).retrieve().body()
由于此端点的响应包含与我们的 Amenity
类匹配的对象列表,因此我们可以首先将整个响应作为 JsonNode
返回,然后再次调用我们的 mapper.readValue
方法。
接下来,我们将遍历数组中的每个对象,从每个对象中创建一个 Amenity
实例。试一试,按照我们在 featuredListingsRequest
方法中实施的相同步骤操作。准备好了就将你的方法与下面的最终状态进行比较!
public List<Amenity> amenitiesRequest(String listingId) throws IOException {JsonNode response = client.get().uri("/listings/{listing_id}/amenities", listingId).retrieve().body(JsonNode.class);if (response != null) {return mapper.readValue(response.traverse(), new TypeReference<List<Amenity>>() {});}return null;}
完成数据提取器
让我们通过调用我们的新方法来完成我们的数据提取器。回到 ListingDataFetcher
中,在 amenities
方法中,我们将使用列表的 id
作为 参数 并返回调用 listingService.amenitiesRequest
的结果。
@DgsData(parentType="Listing")public List<Amenity> amenities(DgsDataFetchingEnvironment dfe) throws IOException {ListingModel listing = dfe.getSource();String id = listing.getId();Map<String, Boolean> localContext = dfe.getLocalContext();if (localContext != null && localContext.get("hasAmenityData")) {return listing.getAmenities();}return listingService.amenitiesRequest(id);}
只需要最后一件事情!我们的 ListingService
对便利设施的请求 可能 会导致可能发生的异常,所以让我们考虑一下这个异常。
@DgsData(parentType = "Listing")public List<Amenity> amenities(DgsDataFetchingEnvironment dfe) throws IOException {// ... amenity fetching logic!}
探索时间:第二轮!
服务器重启,并运行最新的更改了吗?很好!现在,当我们回到沙盒并运行 查询 获取 featuredListings
及其便利设施列表时,我们得到了我们想要的东西!
query GetFeaturedListings {featuredListings {idtitledescriptionamenities {idnamecategory}}}
👏👏👏
与 REST 方法比较
现在再次戴上我们的产品应用程序开发人员帽子!让我们比较一下如果我们使用 REST 而不是 GraphQL,此功能会是什么样子。
如果我们使用 REST,应用程序逻辑将包括
- 对
/featured-listings
端点进行 HTTP GET 调用 - 对响应中的 每个列表 进行额外的 HTTP GET 调用以访问
GET /listings/{listing_id}/amenities
。根据列表的数量,等待所有这些调用解析可能需要一段时间。此外,这引入了 常见的 N+1 问题。 - 仅检索
id
、name
和category
属性,丢弃响应的其余部分。根据响应,这可能意味着我们获取了大量 未使用的 数据!而且大型响应会带来成本。
使用 GraphQL,客户端会编写简洁、干净、可读的 操作,并且数据会以他们指定的精确格式返回,不多不少!
所有提取数据、进行额外 HTTP 调用和过滤所需 字段 的逻辑都在 GraphQL 服务器 端完成。我们仍然有 N+1 问题,但它发生在服务器端(响应和请求速度更加一致,通常更快),而不是客户端(网络速度 变化 且不一致)。
注意:我们可以在 GraphQL 端使用 数据加载器 来解决 N+1 问题,我们将在即将推出的课程中介绍这一点。
主要收获
- 一个 解析器 链是解析特定 GraphQL 操作 时调用数据提取器函数的顺序。它可以包含顺序路径以及并行分支。
- 此链中的每个数据提取器方法都会将其返回值作为名为
DgsDataFetchingEnvironment
的大型对象的source
属性传递给下一个方法。 - 该
DgsDataFetchingEnvironment
对象是一个可选参数,所有数据提取器方法都可以访问。它包含在 解析器 链中调用的前一个数据提取器的返回值,以及有关正在执行的 查询 的其他数据。 - 我们可以使用 DGS 的
DataFetcherResult
来自定义数据提取器方法返回的值,附加本地上下文,链中的下一个数据提取器方法可以访问该上下文。
接下来
对查询有信心了吗?是时候探索 GraphQL 的另一面了:变异。
分享你关于本课的问题和评论
此课程目前处于
你需要一个 GitHub 帐户才能在下面发布。没有? 在我们的 Odyssey 论坛上发布。