实体接口
以多态方式添加实体字段
Apollo Federation为GraphQL接口提供强大的扩展,特别适用于与您的超级图一起使用entities:
- 应用
@key
指令到interface
定义中,使其成为实体接口。 - 在其他子图中,使用
@interfaceObject
指令自动向实现您的实体接口的每个实体添加字段。
借助这些扩展,您的子图可以快速向多个实体贡献一组字段,而无需复制任何现有(或未来的)实体定义。
概述视频
示例模式
让我们看看定义一个Media
实体接口的超级图,以及实现它的Book
实体:
interface Media @key(fields: "id") {id: ID!title: String!}type Book implements Media @key(fields: "id"){id: ID!title: String!}
type Media @key(fields: "id") @interfaceObject {id: ID!reviews: [Review!]!}type Review {score: Int!}type Query {topRatedMedia: [Media!]!}
这个例子虽然简短,但内容丰富。让我们来分解一下
- 子图 A 定义了
Media
接口,以及实现该接口的Book
实体。- Media
接口使用
@key指令,这使得它成为一个
实体接口。 - 这种用法要求所有实现
Media
的对象都是实体,并且这些实体都使用指定的@key
。 - 如上图所示,
Book
是一个实体,并且确实使用了单个指定的@key
。
- Media
- 子图 B 想要为所有实现
Media
的实体添加一个reviews
字段。- 为了实现这一点, Subgraph B 也定义了
Media
,但作为 对象类型。了解更多关于为什么这是必要的。 - Subgraph B 将
@interfaceObject
指令应用于Media
,这表示该对象对应于另一个 子图 的实体接口。 - Subgraph B 对
Media
使用了与 Subgraph A 相同的确切@key
,并且还定义了所有@key
字段(在这种情况下,只是id
)。 - Subgraph B 定义了新的
Media.reviews
字段。 - 子图B还将负责解析
reviews
字段。要了解如何操作,请参阅解析@interfaceObject。
- 为了实现这一点, Subgraph B 也定义了
当组合针对上述子图架构运行时,它会识别子图B的@interfaceObject
。它将新的reviews
字段添加到supergraph架构的Media
接口中,并将该字段添加到实现中Book
实体(以及其他实体)中:
interface Media @key(fields: "id") {id: ID!title: String!reviews: [Review!]!}type Book implements Media @key(fields: "id"){id: ID!title: String!reviews: [Review!]!}type Review {score: Int!}
子图B可能会通过直接向实体贡献字段来添加Book.reviews
。然而,如果我们想将该reviews
字段添加到一百多个实体实现中Media
怎么办?
通过通过@interfaceObject
)添加实体字段,我们可以避免在子图B中重新定义一百个实体(更不用说每当创建一个新的实现实体时都需要添加更多定义了)。了解更多信息。
需求
要使用实体接口和@interfaceObject
,您的supergraph必须满足以下所有要求。如果不满足,组合将失败。
启用支持
如果当前没有,所有的子图架构都必须使用
@link
指令来启用Federation 2特性。任何使用
@interfaceObject
指令或对一个接口应用@key
的子图架构都必须针对Apollo Federation规范v2.3或更高版本:extend schema@link(url: "https://specs.apollo.dev/federation/v2.3"import: ["@key", "@interfaceObject"])此外,使用
@interfaceObject
的架构必须将其包含在上面的@link
指令的import
数组中。
用法规则
interface
定义
假设子图A将MyInterface
类型定义为一个实体接口,以便其他子图可以为其添加字段:
interface MyInterface @key(fields: "id") {id: ID!originalField: String!}type MyObject implements MyInterface @key(fields: "id") {id: ID!originalField: String!}
在这种情况下
- 子图A必须在其
MyInterface
定义中包含至少一个@key
指令。- 它可能包含多个
@key
指令。
- 它可能包含多个
- 子图A必须定义超图中实现
MyInterface
的每个实体类型。- 某些其他子图也可以定义这些实体,但子图A必须定义所有这些实体。
- 您可以想象一个定义实体接口的子图,也可以拥有实现该接口的每个实体。
- 子图A必须能够唯一识别任何实现
MyInterface
的实体实例,仅使用由MyInterface
定义的@key
字段。- 换句话说,如果
EntityA
和EntityB
都实现MyInterface
,则EntityA
的任何实例都不能与EntityB
的任何实例具有完全相同的@key
字段值。 - 这一唯一性要求始终适用于单个实体的实例。通过实体接口,这一要求扩展到所有实现实体的实例。
- 此要求是为了支持在子图A中确定性解析接口。
- 换句话说,如果
- 每个实现
MyInterface
的实体必须包含MyInterface
定义中的所有属性。- 这些实体可以根据需要定义附加的属性。
@interfaceObject
定义
假设子图B将@interfaceObject
应用于名为MyInterface
的对象类型:
type MyInterface @key(fields: "id") @interfaceObject {id: ID!addedField: Int!}
在这种情况下
必须至少有一个其他子图定义一个名为
MyInterface
的接口类型,并应用@key
指令(例如,上面的子图A):子图 Ainterface MyInterface @key(fields: "id") {id: ID!originalField: String!}每个定义
MyInterface
为对象类型的子图必须:- 将其定义应用于
@interfaceObject
。 - 包含与接口类型定义完全相同的
@key
。
- 将其定义应用于
子图B不得将
MyInterface
也定义为接口类型。子图B不得定义任何实现
MyInterface
的实体。- 如果一个 子图 通过
@interfaceObject
提交 实体 字段,它将放弃 向实现该接口的任何单个 实体 提交 字段的 能力。
- 如果一个 子图 通过
必需的解析器
界面
引用解析器
在 上面的示例模式中,子图 A 定义 Media
为一个 实体 接口,并对其应用了 @key
指令:
interface Media @key(fields: "id") {id: ID!title: String!}
与任何 标准实体 类似,@key
指出 "此 子图 可解析提供其 @key
字段 的 此类型任何实例。" 这意味着子图 A 需要为 Media
定义一个引用 解析器,就像为任何其他 实体 一样。
ⓘ 注意
定义引用 解析器的 子图库取决于您使用的库。某些 子图 库可能使用不同的术语来表示此功能。有关详细信息,请参阅您的库文档。
以下是在使用 Apollo Server 并使用 @apollo/subgraph
库时为 Media
的示例引用 解析器:
Media: {__resolveReference(representation) {return allMedia.find((obj) => obj.id === representation.id);},},// ....other resolvers ...
在这个例子中,假设的 变量 allMedia
包含所有 Media
数据,包括每个对象的 id
。
@interfaceObject
解析器
字段解析器
在 上面的示例模式中,子图 B
定义 Media
为一个 对象类型,并将其应用于它。它还定义了 Query.topRatedMedia
字段:
type Media @key(fields: "id") @interfaceObject {id: ID!reviews: [Review!]!}type Review {score: Int!}type Query {topRatedMedia: [Media!]!}
子图 B
需要为新的 topRatedMedia
字段定义一个解析器,以及任何其他返回 Media
类型的字段。
记住:从Subgraph B的角度来看,Media 是一个对象。因此,你必须使用和其他对象相同的逻辑来为其创建解析器。Subgraph B只需要能够解析它所了解的Media
字段(id
和reviews
)。
引用解析器
请注意,在Subgraph B中,Media是一个应用了@key
的object types,因此它是一个标准实体。与任何实体定义一样,它也需要相应的引用解析器:
Media: {__resolveReference(representation) {return allMedia.find((obj) => obj.id === representation.id);},},// ....other resolvers ...
为什么@interfaceObject
是必要的?
没有@interfaceObject
指令及其相关组合逻辑,将接口类型的定义在子图之间分发,可能会对您的子图团队造成持续的维护需求。
让我们看看一个没有使用@interfaceObject
的例子。在这里,Subgraph A定义了Media接口,以及两个实现实体:
interface Media {id: ID!title: String!}type Book implements Media @key(fields: "id") {id: ID!title: String!author: String!}type Movie implements Media @key(fields: "id") {id: ID!title: String!director: String!}
现在,如果Subgraph B想要在Media接口中添加一个reviews
字段,它不能仅仅定义该字段:
❌
interface Media {reviews: [Review!]!}type Review {score: Int!}type Query {topRatedMedia: [Media!]!}
这个添加打破了组合。在supergraph模式中,现在Media接口定义了reviews字段,但Book nor Movie 没有定义!
为了使其有效,Subgraph B还必须将reviews
字段添加到每个实现Media的entities
:
⚠️
interface Media {reviews: [Review!]!}type Review {score: Int!}type Book implements Media @key(fields: "id") {id: ID!reviews: [Review!]!}type Movie implements Media @key(fields: "id") {id: ID!reviews: [Review!]!}
这将解决我们当前的组合错误,但组合将再次在Subgraph A定义实现Media的新的实体时打破:
type Podcast implements Media @key(fields: "id") {id: ID!title: String!}
为了防止这些组合错误,维护Subgraph A和Subgraph B的团队需要在创建每个Media
实现时协调其模式更改。想象一下,如果Media的定义分布在十个子图中!
总的来说,Subgraph B不应该需要知道您的supergraph中存在的所有可能的Media
类型。相反,它应该普遍知道如何检索任何类型的Media
的评论。这正是实体接口和@interfaceObject
提供的关系,如上例所示。
使用@interfaceObject
有其他替代方案吗?
使用@interfaceObject
的主要替代方法是使用前文提到的不可取策略。这需要在每个贡献该接口字段的子图中复制给定接口的所有实现。
请注意,此替代方法还要求每个子图能够解析实现该接口的任何对象的类型。在许多情况下,特定的子图无法完成此任务,这意味着此替代方法不可行。