概述
在本课中,我们将
- 讨论数据加载器是什么
- 回顾数据加载器的要求
- 详细了解数据加载器在引擎盖下的工作原理
数据加载器
为了解决应用程序中的 n+1 问题,我们将使用 数据加载器。
数据加载器的主要工作是使用单个批处理请求替换多个类似的请求。在我们的示例中,我们看到了三个几乎完全相同的请求,它们使用了特定的列表 ID 来返回便利设施数据。通过使用数据加载器,这将变成一个 单个 请求,可以一次获取所有三个列表的数据。
在 DGS 框架中,我们使用 dataFetcher 方法内部的数据加载器。这是因为在解析查询的过程中,特定的 dataFetcher 方法可能会使用不同的参数调用多次。
我们通过以下 查询来说说明这一点。
query GetFeaturedListingsAmenities {featuredListings {idtitleamenities {name}}}
对于该 查询解决的每个特色列表对象,Listing.amenities
dataFetcher 将独立于每个请求进行解析。(这就导致对同一个端点发出三个单独的网络请求!)
我们想要的行为非常不同:我们希望数据加载器收集 所有 参与 查询 的列表 ID(也称为 键),并执行单个请求。
在我们的示例中,这意味着当使用每个列表的 ID 调用Listing.amenities
数据获取器时,它不再直接调用我们的 REST API;相反,它会将参数传递给数据加载器以收集。
当各个列表 ID 收集在同一个列表中时,数据加载器可以承担起调用数据源的职责。它能够一次向所有 ID 的 REST API 端点分派一个请求同时——大大提升性能,不再让数据获取器针对每个发出网络请求!
最棒的是,使用 DGS,我们的数据加载器会自动消除重复的标识符。这意味着如果我们的查询包含多个具有相同 ID 的列表,我们只会请求一次列表设施。
数据加载器需要什么
数据加载器正是我们解决应用程序中性能问题所需的内容,但是它有一项要求需要我们考虑。我们逐一探讨这些要求。
多个对象的一次性数据
让我们设想我们的数据加载器已经收集了我们查询中涉及的所有不同键,并且准备发出数据请求。它接下来需要什么呢?
嗯,如果我们考虑之前用来返回便利设施数据的 REST API 端点,我们会快速看出问题所在:现在,Listing.amenities
数据提取器逐个将每条房源 ID 发送至GET /listings/{listing_id}/amenities
端点,后者只针对单条房源返回数据。
以下是数据加载器依照预期工作的第一项巨大要求:我们需要一个数据源它可以同时解析多个对象请求。实际上,这意味着我们的数据加载器应该能够发送键列表(例如["listing-1", "listing-2", "listing-3"]
),并为所有这些键返回数据。
好消息是我们确实在我们的 REST API 中有一个不同的端点可以使用,我们可以利用它为多个房源请求便利设施数据:GET /amenities/listings
。它接受作为单个字符串合并的多个房源 ID,并一次性为所有房源返回数据。
我们已提供一种方法,在我们的 数据源中利用此新端点。ListingService
中深入了解。
public List<List<Amenity>> multipleAmenitiesRequest(List<String> listingIds) throws IOException {System.out.println("Calling the /amenities/listings endpoint with listings " + listingIds);JsonNode amenities = client.get().uri(uriBuilder -> uriBuilder.path("/amenities/listings").queryParam("ids", String.join(",", listingIds)).build()).retrieve().body(JsonNode.class);if (amenities != null) {return mapper.readValue(amenities.traverse(), new TypeReference<List<List<Amenity>>>() {});}return null;}
注意:如果你来自介绍 GraphQL with Java & DGS,请将此方法复制到你的 ListingService
类中!
此方法设置为接受 List
列出 ID(例如 ["listing-1", "listing-2", "listing-3"]
)。
它将 ID 列表构造为一个字符串,将它们附加为 query 参数 ids
,并向 GET /amenities/listings
端点发出请求。然后它将响应正文接收为 JsonNode
类的一个实例。如果请求成功,我们返回遍历 JsonNode
并将结果映射到一个包含多个 List
和更小的 List
的 Amenity
类型的结果的大 List
中的结果。稍后,我们深入了解原因!)如果 JsonNode
出于任何原因为空,我们直接返回空。
对于每一个键,一个值
当数据加载器在请求中发送键的列表时,它对 数据源 提供数据有非常明确的期望:返回的对象数量绝不能大于请求中发送的键的数量。
我们来分解一下这个期望以及 数据源 如何满足它。
在解析 查询时,我们的数据获取器可能会将三个键传递给数据加载器。数据加载器将它们分组到一个列表中 (["listing-1", "listing-2", "listing-3"]
),并使用它们调用 ListingService
方法 multipleAmenitiesRequest
。它期望什么返回值?好吧,它放入了 三个 键的列表;它期望最多返回 三个 对象的列表!
我们再来看看 multipleAmenitiesRequest
方法的返回类型。
public List<List<Amenity>> multipleAmenitiesRequest(List<String> listingIds) throws IOException {// ...method logic}
它返回类型为 List<List<Amenity>>
的数据。这个类型看起来不太美观,但我们可以将其分解:我们请求其便利设施数据的每个列表都将有一个返回的便利设施 列表;这是因为每个列表可以与多个便利设施相关联。因此,如果我们请求 三个 列表 ID 的便利设施数据,我们的响应应包含 三个 便利设施列表。
数据加载器在其列表(List<String>
)中对键进行分组,并且它也希望响应返回列表;这正是为我们提供外层 List
的代码,该列表封装了每个列表的便利设施列表(List<List<Amenity>>
)!在此,数据加载器处理将每个 List<Amenity>
映射回请求它的键的逻辑。
数据加载器作用域
还有一点很重要,需要记住。数据加载器以及它们在任何一个时间处理的键集合都是针对单个查询的生命周期
这意味着,如果我们对查询列表数据运行一次查询,再运行第二次查询,这两个查询中的键将不会进行一起批处理。相反,每个查询都将分别解决。
厘清了这些概念要点后,让我们将注意力转回到代码上。我们将更新我们的Listing.amenities
数据提取器,享受数据加载器的强大功能!
练习
关键要点
- 数据加载器的优势在于,我们可以将标识符列表(例如 ID)批处理到单个请求中,而不是为每个标识符发送一个单独的请求。
- 在数据加载器正常工作之前,我们的数据源(无论是由其他 API 还是数据库构成)需要实现一种方法才能接受多个键(例如 ID),并返回多个对象。
- 数据加载器从数据源接收到的对象数量不应超过数据加载器收集的键的数量。(例如,如果数据加载器请求三个房源的数据,那么它收回来的房源对象不应多于三个!)
后续内容
我们了解了数据加载器及其在我们应用程序中解决的问题。我们也有一个新的 数据源方法接受 多个列表 ID,并解析多个便利设施对象。接下来,我们将实现收集单个请求中多个列表 ID 的数据加载器逻辑。
分享你对本课的问题和评论
此课程当前为
你需要一个 Github 帐户才能在下方发帖。还没有一个吗? 在我们的 Odyssey 论坛中发帖。