模式合成
了解子图模式如何组合成超级图模式
在 Apollo Federation, 组合 是将一组 子图模式 组合成 超级图模式 的过程:
超级图模式包含您的所有 子图模式 中的 类型 和 字段 定义。它还包含了允许您的 路由器 智能路由到所有不同子图的 GraphQL 操作的元数据。
支持的方法
您可以使用以下任何方法执行模式 合成:
自动使用GraphOS
使用托管联盟,Apollo GraphOS会在您的子图的已发布模式更新时自动执行合成。这使您的运行 路由器可以动态地从Apollo获取最新 超级图模式:
了解如何使用托管联盟进行合成,请参阅快速入门。
使用Rover CLI手动执行
支持Rover CLI的supergraph compose
命令,您可以使用该命令从
rover supergraph compose --config ./supergraph-config.yaml
要了解如何安装 Rover 并使用此命令,请参阅 快速入门。
破坏组合
有时,您的 子图架构 可能以导致 组合 失败的方式冲突。这被称为 破坏组合。
例如,看一下这些两个 子图架构:
❌
type Event @shareable {timestamp: String!}
type Event @shareable {timestamp: Int!}
一个 子图 定义 Event.timestamp
为一个 String
,另一个定义为 Int
。 组合 不知道该使用哪种类型,因此失败。
ⓘ 注意
有关字段返回类型无效不一致的示例,请参阅 不同的共享字段返回类型。
破坏 组合 是联合的非常有用的功能!每当一个团队修改他们的 子图架构 时,这些更改可能与另一个子图冲突。但这个冲突不会影响您的 router,因为组合未能生成一个新的 supergraph schema。这就像一个编译器错误,防止您运行无效的代码。
组合规则
在 Federation 2 中,您的 子图架构 必须遵循所有以下规则才能成功组合成一个 supergraph schema:
- 多个 子图 不能在 对象类型 上定义相同的 字段,除非该字段是 可共享的。
- 共享字段必须在每个定义的子图中都具有兼容的返回类型和兼容的参数类型。
- 有关子图之间兼容性和不兼容性差异的示例,请参阅不同的共享字段。
- 如果多个子图定义了相同的类型,则该类型的每个字段都必须由包含它的每个有效的操作解析。
- 这条规则是最复杂的,也是 Federated 2 最基本的规则。让我们更仔细地看看。让我们更详细地了解一下。
不可解析字段示例
此示例展示了一个共享类型中的字段,它不一定始终可解析(因此因此破坏组合)。
考虑以下这些子图模式:
❌
type Query {positionA: Position!}type Position @shareable {x: Int!y: Int!}
type Query {positionB: Position!}type Position @shareable {x: Int!y: Int!z: Int!}
关于这两个子图的以下信息值得注意:
- 它们都定义了一个共享
Position
类型。 - 它们都定义了一个返回
Position
的顶级查询字段。 - 子图B的
Position
包括了z
字段,而子图A的定义只包括共享的字段x
和y
单独来看,这些子图模式是有效的。然而,如果将它们组合在一起,会破坏组合。为什么呢?
组合过程尝试将不一致的类型定义合并为单个超级图架构定义。在这种情况下,Position
的结果定义与子图B的定义完全匹配:
❌
type Query {# From ApositionA: Position!# From BpositionB: Position!}type Position {# From A+Bx: Int!y: Int!# From Bz: Int!}
根据这个假设的超级图架构,以下查询应该是有效的:
query GetPosition {positionA {xyz # ⚠️ Can't be resolved! ⚠️}}
这里是我们的问题。只有子图A可以解析Query.positionA,因为子图B没有定义这个字段。但是子图A没有定义Position.z
!
如果路由器将此查询发送到子图A,它会返回一个错误。并且如果没有额外的配置,子图B不能解析子图A中z
值的Position
。因此,对于此查询,Position.z
是不可解的。
组合识别这个问题,然后失败。上面的假设的超级图架构永远不会真正生成。
Position.z
是一个不总是可解析的字段示例。那么,我们如何确保这样的字段总是可解析的?
不可解析字段的解决方案
有多种确保共享类型字段始终可解析的解决方案。根据您的用例选择解决方案:
在每个定义该类型的子图中定义该字段。
如果每个定义类型的子图都能解析该类型的所有字段而不会引入复杂性,那么一个简单的解决方案就是在所有这些子图中定义和解析所有字段:
✅
type Position @shareable {x: Int!y: Int!z: Int}
type Position @shareable {x: Int!y: Int!z: Int!}
在这种情况下,如果子图A只关心x和y字段,其对于z的解析器可以始终返回null。
这对于封装简单标量数据的共享类型来说是一个有用的解决方案。
ⓘ 注意
您可以使用@inaccessible 指令逐步向多个子图添加值类型字段,而不会破坏组合。了解更多。
将共享类型定义为实体
✅
type User @key(fields: "id") {id: ID!name: String!}
type User @key(fields: "id") {id: ID!age: Int!}
如果您将共享类型定义为实体,不同的子图可以为该类型定义任意数量的不同字段,只要它们都定义了它的键字段。
这在类型与一个或多个子图可访问的数据存储中的条目密切对应时非常有用(例如,一个Users数据库)。
合并来自多个子图的类型
如果特定的GraphQL类型由不同的子图定义不同,则组合使用以下两种策略之一来合并这些定义:联合或交集。
- 联合:supergraph模式包括该类型的所有子图定义的部分。
- 交集:supergraph模式仅包括每个定义该类型的子图中都存在的类型部分。
组合对特定类型使用的合并策略取决于类型,如下所述。
对象、联合和接口类型
组合始终使用联合策略来合并对象、联合和接口类型。
考虑以下子图模式:
type User @key(fields: "id") {id: ID!name: String!email: String!}union Media = Book | Movieinterface BookDetails {title: String!author: String!}
type User @key(fields: "id") {id: ID!age: Int!}union Media = Book | Podcastinterface BookDetails {title: String!numPages: Int}
当这些子图模式组合时,通过联合合并这三个相应的类型。这在supergraph模式中的类型定义如下:
type User {id: ID!age: Int!name: String!email: String!}union Media = Book | Movie | Podcastinterface BookDetails {title: String!author: String!numPages: Int}
因为 组合 使用联合策略处理这些类型,子图 可以贡献不同的部分,并保证这些部分会出现在组合的超级图模式中。
ⓘ 注意
如果不同的 子图 向一个接口类型贡献不同的 字段,任何 对象类型 实现该接口必须定义来自所有子图的贡献字段。否则,组合 失败。
输入类型和字段参数
组合始终使用交集策略来合并输入类型和字段参数。这确保路由器 never 向一个子图传递未经定义的参数。
考虑以下子图模式:
input UserInput {name: String!age: Int}type Library @shareable {book(title: String, author: String): Book}
input UserInput {name: String!email: String}type Library @shareable {book(title: String, section: String): Book}
这些子图为 UserInput
输入类型定义了不同的字段,并且它们为 Library.book
字段定义了不同的 参数。 在组合使用交集策略合并后,超级图模式定义如下:
input UserInput {name: String!}type Library {book(title: String): Book}
如图所示,超级图模式仅包含两个子图都定义的输入字段和参数。
ⓘ 注意
如果交集策略会省略非空值输入字段或参数,组合会失败。这是因为至少有一个子图需要该字段或参数,如果它从超级图模式中省略,路由器就不能提供它。
在多个子图中定义输入类型和字段参数时,请确保每个子图中每个非空值字段和参数都是一致的。有关示例,请参见 参数。
枚举
如果子图之间的枚举定义不同,组合策略取决于枚举的使用方式:
以下提供这些场景的示例。
枚举组合示例
联合
考虑以下这些子图模式:
enum Color {REDGREENBLUE}type Query {favoriteColor: Color}
enum Color {REDGREENYELLOW}type Query {currentColor: Color}
在这种情况下,Color
枚举被用作至少一个对象字段的返回类型。因此,组合通过联合合并 Color
枚举,以确保所有可能的子图返回值都有效。
结果是超级图模式中的以下类型定义:
enum Color {REDGREENBLUEYELLOW}
交集
考虑以下这些子图模式:
enum Color {REDGREENBLUE}type Query {products(color: Color): [Product]}
enum Color {REDGREENYELLOW}type Query {images(color: Color): [Image]}
在这种情况下,Color
枚举至少用作字段参数(或输入类型字段)的类型。因此,组合通过交集合并 Color
枚举,以确保子图不会接收到不再支持的客户端提供的枚举值。
结果是超级图模式中的以下类型定义:
# BLUE and YELLOW are removed via intersectionenum Color {REDGREEN}
精确匹配
考虑以下这些子图模式:
❌
enum Color {REDGREENBLUE}type Query {favoriteColor: Color}
enum Color {REDGREENYELLOW}type Query {images(color: Color): [Image]}
在这种情况下,颜色
枚举被用作以下两个目的:
- 至少一个对象字段的返回类型
- 至少一个 字段 参数(或输入类型字段)
因此,颜色
枚举的定义必须完全符合每个定义它的子图的定义。完全匹配是合并和交集能产生相同结果的唯一场景。
上面的子图模式无法组合,因为它们对 颜色
枚举的定义不同。
指令
组合处理指令不同,具体取决于它是“可执行”指令还是 “类型系统”指令。
可执行指令
可执行 指令 被设计供客户端在他们的查询中使用。它们应用于一个或多个 可执行指令的位置。例如,你可能有一个 指令 定义 指令 @lowercase on FIELD
,客户端可以在他们的 查询中使用它,如下所示:
query {getSomeData {someField @lowercase}}
只有满足以下所有条件时,可执行 指令 才会被组合到超图模式中:
- 指令在所有 子图 中被定义。
- 指令在所有 子图 中有 identical 的定义。
- 指令不包含在任何
@composeDirective
指令中。
类型系统指令
类型系统 指令帮助定义模式的结构,并且不供客户端使用。它们应用于一个或多个 类型系统指令位置。
这些 指令 不组合到超图模式中,但它们仍然可以通过 @composeDirective
指令提供信息给路由器。