Apollo联盟中的值类型
在多个子图中共享类型和字段
在一个联盟图中图中,想要在多个 GraphQL 类型之间共享是很常见的子图。
例如,假设您想在不同的子图中定义和重用通用Position
类型:
type Position {x: Int!y: Int!}
此类类型被称为值类型。本文描述了如何在联盟图中共享值类型及其字段,使多个子图能够定义并解析它们。
共享对象类型
默认情况下,在Federation 2 子图中,单个对象字段不能被超过一个子图模式定义或解析。
考虑以下Position
示例:
❌
type Position {x: Int!y: Int!}
type Position {x: Int!y: Int!}
尝试将这些两个子图模式组合在一起将会破坏组合。破坏组合。路由器不知道哪个子图负责解析Position.x
和Position.y
。要使多个子图解析这些字段,您必须首先将此字段标记为@shareable
。
ⓘ 注意
作为替代方案,如果您希望子图A和B解析Position
的不同字段,您可以指定Position
类型为实体。
使用 @shareable
该@shareable
指令允许多个子图解析特定对象字段(或一组对象字段)。
要在子图模式中使用@shareable
,您首先需要将以下片段添加到该模式中,以选择加入 Federation 2:
extend schema@link(url: "https://specs.apollo.dev/federation/v2.3",import: ["@key", "@shareable"])
然后,您可以将其应用到一个对象类型,或者应用于该类型的单个字段:
✅
type Position @shareable {x: Int!y: Int!}
type Position {x: Int! @shareabley: Int! @shareable}
ⓘ 注意
将类型标记为@shareable
等效于将所有字段标记为@shareable
,因此上面的两个子图定义是等效的。
现在,子图A和B都可以解析x
和y
字段,我们的子图模式将成功组合成一个超级图模式。
⚠️ 对于 @shareable
的重要注意事项
- 如果一个类型或字段被标记为
@shareable
在任何子图中,它必须在定义它的每个子图中被标记为@shareable
或@external
。否则, 组合 将失败。 - 如果有多个子图可以解析一个字段,请确保每个子图对该字段的 解析器 行为相同。否则,查询的结果可能会因哪个子图解析了该字段而不同。
使用 @shareable
与 extend
如果您将 @shareable
应用于对象类型声明,它仅适用于该声明中的 字段
type Position @shareable {x: Int! # shareabley: Int! # shareable}extend type Position {z: Int! # ⚠️ NOT shareable!}
使用 extend
关键字,上述模式包括对 Position
的两个不同声明。因为只有第一个声明被标记为 @shareable
,所以 Position.z
不被视为可分享的。
要使 Position.z
可分享,您可以执行以下操作之一:
将单个字段
z
标记为@shareable
。extend type Position {z: Int! @shareable}将整个
extend
声明标记为@shareable
。这种策略需要将您的子图模式中的 Apollo Federation 规范版本 targeting v2.2 或更高。早期版本不支持将
@shareable
应用到同一对象类型多次。extend schema@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"])extend type Position @shareable {z: Int!}
不同的共享字段
共享字段只能以特定方式在不同 返回类型 和 参数 中有所不同。如果您要在子图之间共享的字段之间有比允许的更大的差异,请使用 实体 而不是可分享的值类型。
返回类型
假设两个子图都定义了具有一个 timestamp
字段的 Event
对象类型:
❌
type Event @shareable {timestamp: Int!}
type Event @shareable {timestamp: String!}
子图 A的时间戳
返回一个整数
,而子图B返回一个字符串
。这是无效的。当组合 尝试为超级图模式生成事件
类型时,由于两个时间戳
字段定义之间的不可解决的冲突
,它失败。
接下来,看看对位置
对象类型的这些不同定义:
✅
type Position @shareable {x: Int!y: Int!}
type Position @shareable {x: Inty: Int}
子图A中x
和y
字段是非空的,但在子图B中是可为空的。这是有效的。组合 识别出它可以使用下面的定义在超级图模式中为位置
:
type Position {x: Inty: Int}
这个定义适用于对子图A的查询,因为子图A的定义比这个更严格(对于可为空字段,非空值始终有效)。在这种情况下,组合 将子图A的位置
字段转换为满足子图B降低限制性的定义。
ⓘ 注意
子图A的实际子图模式没有修改。在子图A中,x
和y
仍然是非空的。
参数
共享字段参数 在不同子图之间可能以某种方式不同:
- 如果一个参数 至少在一个子图中是必需的,那么它可以在其他子图中是可选的。它不能被省略。
- 如果一个参数 在定义它的每个子图表中都是可选的,那么在它定义的其他子图中省略它是技术上有效的。然而:
- ⚠️ 如果任何子图省略了字段参数,那么该参数将完全省略在超级图模式中。这意味着客户端不能为该字段提供参数。
✅
type Building @shareable {# Argument is requiredheight(units: String!): Int!}
type Building @shareable {# Argument can be optionalheight(units: String): Int!}
❌
type Building @shareable {# Argument is requiredheight(units: String!): Int!}
type Building @shareable {# ⚠️ Argument can't be omitted! ⚠️height: Int!}
⚠️
type Building @shareable {# Argument is optionalheight(units: String): Int!}
type Building @shareable {# Argument can be omitted, BUT# it doesn't appear in the# supergraph schema!height: Int!}
有关更多信息,请参阅输入类型和字段参数 。
省略字段
看看这两个位置
对象类型的定义:
⚠️
type Position @shareable {x: Int!y: Int!}
type Position @shareable {x: Int!y: Int!z: Int!}
子图B定义了一个z
字段,但子图A没有。在这种情况下,当组合 为超级图模式生成位置
类型时,它包含了所有三个字段
:
type Position {x: Int!y: Int!z: Int!}
这个定义适用于子图B,但它对子图A提出了问题。假设子图A定义了以下查询
类型:
type Query {currentPosition: Position!}
根据假定的 supergraph 架构 ,以下 查证 对 supergraph 是有效的:
❌
query GetCurrentPosition {currentPosition {xyz # ⚠️ Unresolvable! ⚠️}}
问题是这样的:如果子图 B 没有定义 Query.currentPosition
,那么必须对子图 A 执行该 查证。但子图 A 缺少 Position.z
字段,因此无法解决该字段!
认识潜在问题,并以错误失败。那么我们如何解决这个问题?请查看 不可解决字段的解决方案。
添加新共享字段
将新字段添加到值类型可能会导致 组合 问题,因为很难同时将该字段添加到所有定义 子图。
假设我们正在将 z
字段添加到我们的 Position
值类型,并且我们从子图 A 开始:
⚠️
type Position @shareable {x: Int!y: Int!z: Int!}
type Position @shareable {x: Int!y: Int!}
可能当我们尝试组合这些两个架构时, 组合 将失败,因为子图 B 无法解决 Position.z
.
要逐步将字段添加到所有子图而不会破坏 组合,我们可以在其中使用 @inaccessible
指令。
使用 @inaccessible
如果将 @inaccessible
指令应用于一个字段, 组合 将会省略该字段从您的 API 架构 的 路由器。 这有助于您逐步将一个字段添加到多个子图而不会破坏组合。
要在子图中使用 @inaccessible
,首先请确保将其包含在 Federation 2 的 import
声明中的
extend schema@link(url: "https://specs.apollo.dev/federation/v2.3",import: ["@key", "@shareable", "@inaccessible"])
每次将新字段添加到值类型时,如果该字段不是在每个定义值类型的子图中都存在,则将 @inaccessible
应用到该字段:
type Position @shareable {x: Int!y: Int!z: Int! @inaccessible}
type Position @shareable {x: Int!y: Int!}
即使 Position.z
在多个子图中已定义,您也只需在其中一个子图中应用 @inaccessible
来省略它。事实上,您可能只想在其中一个子图中应用它,以便以后更容易删除它。
使用上述语法, 组合 将省略生成的 API
的 架构中的 Position.z
,并且生成的 Position
类型将仅包括 x
和 y
字段。
ⓘ 注意
请注意,Position.z
在supergraph架构中确实存在,但API架构强制规定客户端可以包含哪些字段在操作中。了解更多关于联邦架构的信息。
随时您都可以现在将Position.z
添加到子图B:
type Position @shareable {x: Int!y: Int!z: Int! @inaccessible}
type Position @shareable {x: Int!y: Int!z: Int!}
此时,Position.z
仍然是@inaccessible
,因此组合仍然忽略它。
最后,当您已将Position.z
添加到定义Position
的每个子图时,您可以从子图A中删除@inaccessible
:
type Position @shareable {x: Int!y: Int!z: Int!}
type Position @shareable {x: Int!y: Int!z: Int!}
组合现在已成功在supergraph架构中包含Position.z
!
联合和接口
Federation 2中,默认情况下,union
和interface
类型定义可以在子图之间共享,并且那些定义可以不同:
union Media = Book | Movieinterface User {name: String!}
union Media = Book | Podcastinterface User {name: String!age: Int!}
组合逻辑在您的supergraph架构中合并这些定义:
union Media = Book | Movie | Podcast# The object types that implement this interface are# responsible for resolving these fields.interface User {name: String!age: Int!}
当不同的子图负责一组相关类型或值的子集时,这可能非常有用。
ⓘ 注意
您还可以在多个子图之间使用enum
类型。有关详细信息,请参阅合并来自多个子图的类型。
共享接口的挑战
在子图之间共享接口类型会在接口变化时引入维护挑战。考虑以下子图:
interface Media {id: ID!title: String!}type Book implements Media {id: ID!title: String!}
interface Media {id: ID!title: String!}type Podcast implements Media {id: ID!title: String!}
现在,假设子图B向Media
接口添加一个creator
字段:
❌
interface Media {id: ID!title: String!}type Book implements Media {id: ID!title: String!# ❌ Doesn't define creator!}
interface Media {id: ID!title: String!creator: String!}type Podcast implements Media {id: ID!title: String!creator: String!}
这打破了组合,因为Book
也实现了Media
但未定义新的creator
字段。
为了避免此错误,所有子图中的实现类型都需要更新,以包含所有Media
字段。随着您的子图和团队数量增长,这变得越来越具有挑战性。幸运的是,有一种解决方案。
解决方案:实体接口
apol delenation 2.3引入了一种强大的抽象机制,使您能够在不需要更新每个实现类型的情况下,向子图添加接口字段。
输入类型
子图可以共享 输入类型定义,但组合使用交集策略合并它们的字段。当输入类型跨多个子图组合时,只有互相的字段在超级图模式中保留:
input UserInput {name: String!age: Int # Not in Subgraph B}
input UserInput {name: String!email: String # Not in Subgraph A}
组合逻辑只合并所有输入类型都有的字段。了解更多信息,请参阅合并输入类型和字段参数。
input UserInput {name: String!}
要了解更多关于组合如何在底层合并不同模式类型的信息,请参阅在组合期间合并类型。