第二部分:了解解析器
在第一部分中,我们开发了一个非常简单的 GraphQL 服务器。该解决方案有一个严重的缺陷:所有字段都急切地加载到后端,即使前端未要求也是如此。通过不给客户任何选择,我们通过 RESTful 服务来接受这种情况。RESTful API 始终返回所有内容,这意味着始终加载所有内容。另一方面,如果将 RESTful API 分为多个较小的资源,则可能会面临 N + 1 问题和多次网络往返的风险。GraphQL 是专门为解决以下问题而设计的:
- 仅获取必需的数据,以避免额外的网络流量以及后端不必要的工作
- 允许在单个请求中获取客户端所需的尽可能多的数据,以减少总体延迟
RESTful API 可以任意决定要返回多少数据,因此几乎无法解决上述问题。它要么获取过多,要么获取不足。好的,这是理论,但是我们对 GraphQL 服务器的实现不能以这种方式工作。无论是否请求,它仍将获取所有数据。伤心。
不断发展您的 API
要回顾一下我们的 API 返回一个实例Player
DTO:
@Value
class Player {
UUID id;
String name;
int points;
ImmutableList<Item> inventory;
Billing billing;
}
与此 GraphQL 模式匹配的:
type Player {
id: String!
name: String!
points: Int!
inventory: [Item!]!
billing: Billing!
}
通过仔细地分析我们的应用程序,我意识到很少有客户 billing
在他们的查询中提出要求,但是我们必须始终提出要求 billingRepository
才能创建 Player
实例。许多急切的,不需要的工作:
private final BillingRepository billingRepository;
private final InventoryClient inventoryClient;
private final PlayerMetadata playerMetadata;
private final PointsCalculator pointsCalculator;
//...
@NotNull
private Player somePlayer() {UUID playerId = UUID.randomUUID();
return new Player(
playerId,
playerMetadata.lookupName(playerId),
pointsCalculator.pointsOf(playerId),
inventoryClient.loadInventory(playerId),
billingRepository.forUser(playerId)
);
}
像这样的字段 billing
只能在请求时加载!为了了解如何使我们的对象的某些部分_图_(Graph-QL 是也)懒加载,让我们添加一个名为新特性 trustworthiness
到Player
上:
type Player {
id: String!
name: String!
points: Int!
inventory: [Item!]!
billing: Billing!
trustworthiness: Float!
}
此更改是向后兼容的。事实上,GraphQL 实际上没有 API 版本控制的概念。那么迁移路径是什么?有以下几种情况:
- 您错误地将新架构提供给客户端,而没有实现服务器。在这种情况下,客户端会快速失败,因为它请求
trustworthiness
服务器尚未交付的字段。好。另一方面,使用 RESTful API,客户端认为服务器将返回一些数据。这可能会导致意外错误或服务器故意返回的假设null
(丢失字段) - 您添加了
trustworthiness
字段,但没有分发新的架构。还行吧。客户不知道,trustworthiness
所以他们不要求它。 - 服务器准备就绪后,便将新架构分发给客户端。客户端可能会或可能不会使用新数据。没关系。
但是,如果您犯了一个错误并向所有客户端宣布服务器的新版本支持某些架构而实际上却不支持该架构,该怎么办?换句话说,服务器假装为 support trustworthiness
,但是在被询问时它不知道如何计算。这有可能吗?否:
Caused by: [...]FieldResolverError: No method or field found as defined in schema [...] with any of the following signatures [...], in priority order:
com.nurkiewicz.graphql.Player.trustworthiness()
com.nurkiewicz.graphql.Player.getTrustworthiness()
com.nurkiewicz.graphql.Player.trustworthiness
这是在服务器启动时发生的!如果您在不实施底层服务器的情况下更改了架构,它甚至将无法启动!这真是个好消息。如果您宣布支持某些架构,则不可能发布不支持该架构的应用程序。改进 API 时,这是一个安全网。仅当服务器支持架构时,才将架构交付给客户端。并且,当服务器宣布某些架构时,您可以 100%确保其正常工作并正确格式化。响应中没有其他缺少的字段,因为您正在询问服务器的旧版本。不再有假装支持某些 API 版本的损坏服务器,而实际上,您忘记了将字段添加到响应对象。
用懒加载的 Resolver
代替贪婪加载值
好了,那么我该如何添加 trustworthiness
以遵守新的架构?在_不那么聪明的_技巧是正确的,在该阻止我们的应用程序启动异常。它说它正在尝试寻找方法,获取器或字段 trustworthiness
。如果我们盲目地将其添加到Player
类中,API 将会起作用。那是什么问题 请记住,在更改架构时,旧客户端不知道 trustworthiness
。即使知道这一点的新客户,也可能永远不会或很少要求它。换句话说,trustworthiness
只需要为一小部分请求计算出值。不幸的是,因为 trustworthiness
是现场上 Player
课,我们必须总是热切计算。否则,无法实例化并返回响应对象。有趣的是,使用 RESTful API,这通常不是问题。只需加载并返回所有内容,让客户决定忽略什么。但是我们可以做得更好。
首先,trustworthiness
从Player
DTO 中删除字段。我们必须更深入,我是说懒惰。而是,创建以下组件:
import com.coxautodev.graphql.tools.GraphQLResolver;
import org.springframework.stereotype.Component;
@Component
class PlayerResolver implements GraphQLResolver<Player> {}
保持空白,GraphQL 引擎将指导我们。当尝试再次运行该应用程序时,该异常很熟悉,但并不相同:
FieldResolverError: No method or field found as defined in schema [...] with any of the following signatures [...], in priority order:
com.nurkiewicz.graphql.PlayerResolver.trustworthiness(com.nurkiewicz.graphql.Player)
com.nurkiewicz.graphql.PlayerResolver.getTrustworthiness(com.nurkiewicz.graphql.Player)
com.nurkiewicz.graphql.PlayerResolver.trustworthiness
com.nurkiewicz.graphql.Player.trustworthiness()
com.nurkiewicz.graphql.Player.getTrustworthiness()
com.nurkiewicz.graphql.Player.trustworthiness
trustworthiness
不仅在 Player
类上,而且还在 PlayerResolver
我们刚刚创建的上寻找。您能发现这些签名之间的区别吗?
PlayerResolver.getTrustworthiness(Player)
Player.getTrustworthiness()
前一种方法 Player
作为参数,而后一种方法 Player
本身就是实例方法(getter)。目的是 PlayerResolver
什么?默认情况下,GraphQL 模式中的每种类型都使用默认解析器。该解析器基本上采用例如的实例,Player
并检查吸气剂,方法和字段。但是,您可以使用更复杂的默认解析器来装饰该默认解析器。一种可以懒惰地计算给定名称的字段。尤其是在 Player
课堂上没有这样的领域时。最重要的是,仅当客户端实际请求该字段时才调用该解析程序。否则,我们将退回到默认解析器,该解析器期望所有字段都属于 Player
对象本身。那么,您如何实现自定义解析器 trustworthiness
呢?该异常将指导您:
@Component
class PlayerResolver implements GraphQLResolver<Player> {float trustworthiness(Player player) {
//slow and painful business logic here...
return 42;
}
}
当然,在现实世界中,实现会做一些聪明的事情。采取Player
,应用一些业务逻辑,等等。真正重要的是,如果客户不想知道trustworthiness
,永远不会调用此方法。懒!通过添加一些日志或指标来自己查看。是的,指标!这种方法还使您可以深入了解自己的 API。客户非常明确,只询问必填字段。因此,您可以为每个解析器获取指标,并快速找出哪些字段已使用,哪些字段无效以及可以弃用或删除。另外,您可以轻松地发现哪个特定字段的加载成本很高。使用 RESTful API 的全有或全无的方法,这种细粒度的控制是不可能的。为了使用 RESTful API 停用字段,您必须创建资源的新版本并鼓励所有客户端迁移。
懒惰的一切
如果您想变得更加懒惰并消耗尽可能少的资源,则 Player
可以将的每个单个字段委派给解析器。模式保持不变,但 Player
类变为空心:
@Value
class Player {UUID id;}
那么,如何 GraphQL 知道如何计算 name
,points
,inventory
,billing
和trustworthiness
?好吧,解析器上有一个方法可以解决以下每个问题:
@Component
class PlayerResolver implements GraphQLResolver<Player> {String name(Player player) {//...}
int points(Player player) {//...}
ImmutableList<Item> inventory(Player player) {//...}
Billing billing(Player player) {//...}
float trustworthiness(Player player) {//...}
}
实现并不重要。重要的是惰性:只有在请求某些字段时才调用这些方法。这些方法中的每一个都可以分别进行监视,优化和测试。从性能角度来看,这很棒。
性能问题
您是否注意到 inventory
和billing
字段互不相关?即,获取 inventory
可能需要调用某些下游服务,而 billing
需要 SQL 查询。不幸的是,GraphQL 引擎将响应组合成一个顺序。我们将在下一期中解决此问题,敬请期待!