如何避免 GraphQL 接口继承将产品 B 的继承关系引入产品 A?
应将产品 B 的继承关系与两组实体的映射关系分开建模。只要 interface2 继承了包含 A1 字段的 interface1,就必须包含这些字段,因此无法同时表达“A2 不继承 A1”。
可以把 A、B 两侧的数据放在映射对象的不同字段下,让继承只发生在 B 侧。如果必须保留扁平结构,就需要重新划分接口,让父接口只包含适用于所有实现类型的字段。
为什么合并接口会引入错误的字段
GraphQL interface 规定了实现类型必须提供的字段。子接口必须声明父接口要求的字段,并满足相应的类型约束。
当前结构实际意味着 interface1 中的所有字段都适用于 interface2。但 interface1 混合了 A1 和 B1 的字段,业务上却只有 B2 继承 B1,两者并不一致。
这不会让产品 A 的底层实体真正产生继承关系,但会使 GraphQL schema 暴露不适用于 A2 的字段。如果 schema 生成器自动复制父接口字段,A1 新增的自定义字段也会出现在 A2 对应的查询类型中。
推荐方案:分开表达映射与继承
可以用映射对象连接 A、B 两侧的实体,在 B 侧单独保留接口继承。下面的字段仅用于说明结构,并非原问题中的实际字段:
interface BBase {
id: ID!
b1Field: String
}
interface BChild implements BBase {
id: ID!
b1Field: String
b2Field: String
}
type B1 implements BBase {
id: ID!
b1Field: String
}
type B2 implements BBase & BChild {
id: ID!
b1Field: String
b2Field: String
}
type A1 {
id: ID!
a1Custom: String
}
type A2 {
id: ID!
a2Field: String
}
type Mapped1 {
a: A1
b: B1
}
type Mapped2 {
a: A2
b: B2
}
type Query {
mapped1(id: ID!): Mapped1
mapped2(id: ID!): Mapped2
}
Mapped1 和 Mapped2 表示实体映射,两者没有继承关系。BChild 继承 BBase,用于表达 B 侧的类型层次;A1 和 A2 则各自声明自己的字段。
GraphQL 对象类型之间不能直接继承,因此示例通过接口表达 B 侧的继承契约。使用这段 SDL 的前提是所用 GraphQL 实现支持接口实现接口;对象类型还需要显式列出其实现的父接口,并声明相应字段。
查询 A2、B2 对应的数据时,可以使用:
query {
mapped2(id: "example-id") {
a {
id
a2Field
}
b {
id
b1Field
b2Field
}
}
}
b1Field 仍可通过 B2 查询。由于 A2 没有定义 a1Custom,在 mapped2.a 下查询该字段会在 GraphQL 校验阶段失败。
如果必须保留扁平查询结构
可以继续生成扁平的映射类型,但不要让第二组映射类型继承包含 A1 专属字段的接口。接口应按字段适用范围划分:
- 公共接口只包含所有相关类型都具有、且业务含义相同的字段。
- A1 的专属字段只放在 A1 对应的类型或接口中。
- B1 的可继承字段放在 B 侧接口中,由 B1、B2 对应的映射类型分别实现该接口。
例如,两组映射类型都可以实现只包含 B1 契约的 BBase,第二组再实现 BChild。这样既保留了 B 侧的多态查询,也不会要求第二组包含 A1 的自定义字段。
扁平结构还需要处理字段重名。如果 A、B 两侧存在同名但含义或类型不同的字段,单个扁平字段无法完整表达两套定义,应增加前缀或改用嵌套结构。
自定义字段生成时需要检查什么
修正接口关系后,schema 生成规则也需要调整:A1 新增字段时,只更新 A1 及确实包含 A1 的映射类型;B1 新增字段时,依据产品 B 的继承规则更新 B2 对应的接口和类型。
即使让 resolver 对不适用的字段返回 null,该字段仍会保留在 schema 中。要让 A2 根本无法查询 A1 的字段,需要从 A2 对应的字段定义和接口契约中移除它。
原问题没有提供所用的 GraphQL 框架或 schema 生成器,具体要修改哪些配置或生成代码,还需要结合实现确定。应避免继续用一条合并后的接口继承链表达两个产品不同的类型关系。
备注:内容仅供参考。
