对@defer的Router支持
通过增量交付字段来提高性能
发送到的查询GraphOS Router 或 Apollo Router Core 可以使用 @defer
指令 来启用响应数据的增量交付。通过延迟一些 字段 的数据,
router 能够更快地解析并返回查询的 其他 字段 数据,提高响应速度。该 router's @defer
支持与所有
什么是 @defer
?
指令 @defer
允许客户端查询指定它不需要立即接收数据的 field 集合。当查询中的一些 fields 的解析所需时间远长于其他时,这很有用。
延迟的 fields 总是包含在 GraphQL fragment 中,并且对该 @defer
指令应用在 fragment 上(而不是单个 fields 上)。
下面是一个使用 @defer
的查询示例:
query GetTopProducts {topProducts {idname... @defer {price}}}
关于 @defer 的基础
执行 @defer 查询
Accept: multipart/mixed;deferSpec=20220824, application/json
注:因为部件始终是JSON,所以\r\n--graphql
绝对不可能出现在部件的内容中。为了方便起见,服务器可以使用graphql
作为一个边界。客户端必须在Content-Type
中接受服务器返回的任何边界。
路由器是如何延迟字段的?
如在这篇文章中所述,路由器可以延迟以下字段在您的模式中:)
- Query类型的整个字段(包括它们的子字段)
- 字段的任何实体的类型(包括它们的子字段)
路由器可以专门延迟这些字段,因为它们都是进入您的子图之一的一个切入点。这使得路由器能够直接将其纳入生成的查询计划中。
查询计划示例
考虑一个超级图,包含以下子图:)
type Product @key(fields: "id") {id: ID!name: String!price: Int!}type Query {topProducts: [Product!]!}
type Product @key(fields: "id") {id: ID!reviews: [Review!]!}type Review {score: Int!}
并考虑对那个超级图执行的这个查询:)
query GetTopProductsAndReviews {topProducts { # Resolved by Products subgraphidnamereviews { # Resolved by Reviews subgraphscore}}}
为了解析所有这些字段,路由器需要查询产品子图和评论子图。不仅如此,路由器还需要先查询产品子图,这样它就知道要为哪些产品检索评论。
当路由器接收到这个查询时,它会生成一个可以在其子图上运行的子查询序列,以解析所有请求的字段。这个序列被称为查询计划。
以下是示例查询的查询计划的可视表示:
此查询计划有三个步骤:
- 路由器查询产品子图以检索每个顶级产品的
id
和name
。 - 路由器查询评论子图—提供每个顶级产品的
id
—以检索与这些产品对应的评论分数。 - 路由器将两个子查询的数据合并成一个单一的响应并将其返回给客户端。
因为第二个子查询依赖于第一个的数据,这两个子查询必须按顺序发生。
但是,第一个子查询的结果包含客户请求的大量数据!为了提高响应性,从理论上讲,路由器可以在这些数据可用时立即返回这部分。
一个兼容 defer 的客户端可以使用 @defer
指令请求这种行为:
query GetTopProductsAndDeferReviews {topProducts {idname... @defer {reviews {score}}}}
通过这个 查询,路由器会理解它 可以立即返回其第一个子查询的结果,而不是等待第二个子查询的结果。稍后,当第二个子查询准备好的时候,它会返回该子查询的结果。
记住,路由器之所以可以选择性地推迟 Product.reviews
字段,是因为它是实体的一个字段。查询计划已经将实体字段用作它们子查询的入口点,路由器利用这种行为来实现 defer 支持。
单个子图中defer
在前面例子中,一个客户端在需要执行多个子查询的查询中defer了字段。但如果客户端查询的所有字段都属于一个单一子图呢?
考虑这个客户端查询:
query GetTopProducts {topProducts { # All fields resolved by Products subgraphidnameprice}}
因为这些请求的字段都是定义在单个子图中的,所以路由器默认生成最简单的可能查询计划,只有一个步骤:
现在,让我们假设 Product.price
字段需要比其他 Product
字段更长的时间来解析,而查询客户端希望像这样defer它:
query GetTopProducts {topProducts {idname... @defer {price}}}
这是有效的!当路由器看到这个defer请求时,它会为查询生成一个 不同的 查询计划:
现在,路由器会查询两次相同的子图,首先获取非defer字段,然后获取defer字段。当第一个子查询返回时,路由器可以立即返回每个产品的 id
和 name
,同时发送后续的子查询来获取 price
信息。
不可defer的字段
一个查询的 @defer
片段可能包含路由器 不能 defer 的字段。路由器使用以下逻辑优雅地处理这种情况:
- 路由器defer了片段中它可以defer的每个字段。
- 路由器在向客户端发送初始响应之前,就解决了片段中不可defer的字段。
- 路由器对客户端的响应仍然使用多部分编码来分隔
@defer
片段字段和其它字段,即使某些片段字段无法defer。- 这保留了客户端期望的响应结构,基于其使用
@defer
。
- 这保留了客户端期望的响应结构,基于其使用
示例
为了说明一个不可延迟的字段,让我们看一个使用此 子图模式的例子:
type Book @key(fields: "id") {id: ID!title: String!author: Author!}type Author {name: String!books: [Book!]!}type Query {books: [Book!]!authors: [Author!]!}
在此模式中,请注意 Book
类型是一个实体,而 Author
类型 不是。
假设客户端执行以下 查询:
query GetAuthors {authors {name... @defer {books { # Can't be deferredtitle # CAN be deferred}}}}
此 查询 试图延迟两个 字段: Author.books
和 Book.title
。
Author.books
不是一个根Query
字段,也不是实体字段(Author
不是实体),所以路由器 无法 延迟它。Book.title
是实体类型字段的属性,所以路由器 可以 延迟它。- 如果
Book.title
有任何子 字段,路由器也可以延迟这些 字段。
- 如果
在这种情况下,路由器必须先内部解决每个作者相关联的书目列表,然后它才能向客户端发送初始响应。稍后,它可以解析每本书的 标题 并以响应的增量部分将这些 Book 对象返回到客户端。
规范状态
当前 @defer
指令是 GraphQL 规范草案阶段的 RFC 的一个部分(了解 RFC 贡献阶段)。
路由器支持所述的 @defer
指令,该指令已根据2022-08-24的编辑状态记录在 这些对 RFC 的编辑中。
禁用 @defer
。
默认情况下,路由器已启用延迟支持。要禁用支持,请将disable支持添加到您的router's YAML配置文件中的supergraph
键之下:
supergraph:defer_support: false