Hasura GraphQL Engine 的 Apollo Federation v1 支持:从 RFC 设计到源码实现
【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine
导读
本文以 rfcs/apollo-federation.md 为主体,系统讲解 Hasura GraphQL Engine(下称 HGE)如何将自身接入 Apollo Federated Gateway 成为可被其他子图消费的联邦子图。文章覆盖该特性的设计动机、*_track_table元数据配置、_service/_entities联邦字段的 schema 生成与查询求值原理,并结合server/src-lib下的 Haskell 源码与tests-py测试用例给出可验证的实现级依据。读完本文,你将理解 HGE 联邦子图模式的工作原理,并能独立完成表的联邦接入配置与端到端验证。
背景与目标:为什么要让 HGE 支持 Apollo Federation
Apollo Federation 是 Apollo 提出的 GraphQL 联邦架构规范:多个"子图"(subgraph)服务各自维护一部分 schema,由一个"网关"(gateway)将它们编排成统一的联邦 schema。要让 HGE 以子图身份挂载到 Apollo Federated Gateway,并让其他子图能够引用 HGE 中由数据库表生成的类型,RFC 提出了两个核心需求(对应 rfcs/apollo-federation.md 的 "Requirements" 一节):
- HGE 应能挂载到 Apollo Federated Gateway 上——即网关能够通过
_service字段获取 HGE 的 SDL,完成 schema 合并; - 其他子图可以引用 HGE 的表类型——即 HGE 为参与联邦的表生成
@key指令,并支持_entities查询,使网关能够按主键跨子图解析实体。
Apollo 官方规范要求一个 GraphQL 服务成为子图需满足四件事(RFC "Spec" 一节引用):
- 实现 federation schema 规范;
- 支持获取服务能力(即
_service { sdl }); - 为引用实现 stub 类型生成(
_Entityunion); - 实现实体的请求解析(
_entities查询)。
RFC 将整个特性规划为 v1 实验性功能:第一版只要求"能用",后续版本再渐进增强。
启用方式与元数据配置
两级开关
RFC 设想联邦支持通过环境变量或全局元数据字段开启;从当前源码看,该开关最终落地为两个层次:
- 服务级开关:
HASURA_GRAPHQL_ENABLE_APOLLO_FEDERATION环境变量或--enable-apollo-federation命令行参数。同时apollo_federation也被列为一个 experimental feature(见 server/src-lib/Hasura/Server/Init/Arg/Command/Serve.hs 中 "experimental features" 帮助文本,其中注明apollo_federation: use hasura as a subgraph in an Apollo gateway (deprecated)——即该实验特性开关已被显式配置取代)。 - 表级开关:在
*_track_table元数据 API 中为单个表开启联邦。
两级开关的组合逻辑在 ApolloFederation.hs 的getApolloFederationStatus中实现:若用户显式设置了ApolloFederationStatus,则以显式值为准;否则回退到 experimental feature 标志(EFApolloFederation是否在集合中)。
*_track_tableAPI 的扩展
RFC 给出了开启单表联邦的请求示例:
{ "source": "default", "table": "Author", "configuration": {}, "apollo_federation_config": { "enable": "v1" } }这一 API 设计在当前源码中得到完整落地。在 server/src-lib/Hasura/RQL/Types/Common.hs 中定义了对应类型:
data ApolloFederationVersion = V1 deriving (Show, Eq, Generic) -- enable 字段目前只接受 "v1", -- 其余值会报错:"enable takes the version of apollo federation. Supported value is v1 only." data ApolloFederationConfig = ApolloFederationConfig { enable :: ApolloFederationVersion }TableMetadata的 codec 在 server/src-lib/Hasura/Table/Metadata.hs 中增加了可选字段apollo_federation_config。而isApolloFedV1enabled :: Maybe ApolloFederationConfig -> Bool直接判断该配置是否存在(isJust),即只要配置了enable: v1即视为启用。
RFC 指出这种"配置对象内放置键值对"的 API 设计便于未来扩展,例如:
- 未来支持 v2 指令(如为某些列添加
@sharable); - 允许自定义
@key指令的fields(即联邦主键,默认取表主键)。
行为:自动添加@key指令
当一张表以开启联邦的方式被 track 后,该表在 schema 中的类型会被自动加上@key指令,字段值取自表主键。RFC 以Review表为例:
type Review @key(fields: "id") { id: Integer! body: String author: User product: Product }@key(fields: "id")自动生成,其中id是Review表的主键列。这正是"其他子图可以引用 Hasura 表类型"的关键机制:网关依据@key的字段值构造_Anyrepresentation,再通过_entities查询按主键取回实体。
实现原理一:SDL 生成与_service字段
从 SchemaIntrospection 到 SDL
为了让网关能拉取子图 schema,HGE 需要暴露_service字段,其类型_Service包含一个sdl: String字段。RFC 提出的方案是:在构建 schema 字段解析器的同时生成 SDL,并给出了核心思路:
getSchemaDocument :: G.SchemaIntrospection -> G.SchemaDocument getSchemaDocument (G.SchemaIntrospection typeDefMap) = G.SchemaDocument completeSchema where allTypeDefns = map G.TypeSystemDefinitionType (Map.elems typeDefMap) rootOpTypeDefns = getRootOpTypeDefns -- define this completeSchema = rootOpTypeDefns : allTypeDefns generateSDL :: G.SchemaIntrospection -> Text generateSDL = Builder.run . Printer.schemaDocument . getSchemaDocument这一设计在当前源码中已具体化为generateSDLFromIntrospection(见 ApolloFederation.hs):
- 遍历
SchemaIntrospection中所有类型定义,并过滤掉以__前缀开头的 GraphQL schema 内建类型(filterTypeDefinition); - 从 introspection 中查找
query_root、mutation_root、subscription_root三个根操作类型,生成RootOperationTypeDefinition; - 最终组装成
G.SchemaDocument,用graphql-parser库的Printer.schemaDocument渲染为Text。
该模块同时提供两个导出函数:generateSDL(移除 schema 内建类型)与generateSDLWithAllTypes(保留全部类型),后者可用于支持未来 Apollo Federation v2(源码中已预留@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])的扩展点,注释标明这是为 v2 指令预留)。
_service字段解析器
mkServiceField创建_service的FieldParser:其内部定义sdl字段解析器(类型String,描述为 "SDL representation of schema"),再以selectionSet组装出_Service类型,最终生成一个接收SchemaIntrospection、返回QueryRootField的解析器。由于sdl的取值依赖 schema introspection,该FieldParser被定义为G.SchemaIntrospection -> QueryRootField UnpreparedValue的函数形态,在 schema 构建期与实际查询期之间传递 introspection。
一个值得强调的细节(RFC 中明确说明):schema introspection 是角色相关的,因此生成的 SDL 不会暴露当前角色无权访问的字段与类型,联邦 SDL 天然遵循权限边界。
RFC 中的完整 SDL 示例
RFC 以一张名为users、含id/name字段、采用graphql-default命名约定的表为例,给出了生成 SDL 的缩略版与完整版。缩略版如下(完整版见 RFC 原文 rfcs/apollo-federation.md):
schema { query: query_root mutation: mutation_root subscription: subscription_root } type query_root { usersAggregate( where: UsersBoolExp orderBy: [UsersOrderBy!] limit: Int offset: Int distinctOn: [UsersSelectColumn!] ): UsersAggregate! users( where: UsersBoolExp orderBy: [UsersOrderBy!] limit: Int offset: Int distinctOn: [UsersSelectColumn!] ): [Users!]! usersByPk(id: Int!): Users } type Users @key(fields: "id") { id: Int! name: String! }完整版 SDL 还包含__Schema、__Type等 introspection 类型(在generateSDL中会被过滤,而generateSDLWithAllTypes会保留)、query_root/subscription_root/mutation_root三个根类型、UsersAggregate及其聚合辅助类型、orderBy/UsersSelectColumn/UsersConstraint等枚举、IntComparisonExp/StringComparisonExp/UsersBoolExp等输入类型,以及UsersInsertInput/UsersOnConflict/UsersPkColumnsInput等变更输入类型。整个 SDL 完整覆盖了 HGE 为该表生成的全部 GraphQL 能力。
实现原理二:_entities查询与_Entityunion
联邦规范要求的字段形态
RFC 给出了_entities字段的规范形态:
# a union of all types that use the @key directive scalar _Any union _Entity extend type Query { _entities(representations: [_Any!]!): [_Entity]! }_Any是一个标量,每个 representation 至少包含__typename与@key指定的主键字段。在当前源码中,_Any的解析由anyParser实现(见 ApolloFederation.hs):它要求输入必须是 JSON 对象,提取__typename键(缺失或非字符串均报解析错误),其余键值对作为afPKValues(主键值)保存,构造出ApolloFederationAnyType { afTypename, afPKValues }。
参与 union 的类型来源
RFC 指出,带@key指令的类型可能来自三类来源:
- DB 表:为
select类型加上@key指令,字段默认取表主键(后续可允许用户自定义); - Actions:需要先扩展
set_custom_typesAPI 以支持 directives(列为未来工作); - Remote Schema:上游 directives 已被存储,可直接复用。
在 v1 实现中,_Entityunion 的成员在 server/src-lib/Hasura/GraphQL/Schema/Build.hs 中收集:对每张表,若isApolloFedV1enabled成立,则基于该表的 selection set、select 权限、主键列等信息构造convertToApolloFedParserFunc,产出(objectTypename, parser)二元组。objectTypename由getTableIdentifierName+mkTableTypeName生成(与常规 GraphQL 表类型名一致),并遵循 naming case 约定。
union 解析器与权限裁剪
union 的Parser通过P.selectionSetUnion Name.__Entity (Just "A union of all types that use the @key directive") entityParserMap创建。RFC 特别提醒:需要根据角色权限移除对应的Parser——如果某角色无权访问@key指令涉及的字段,则应省略该类型的解析器。这一约束在Build.hs中体现为runMaybeT中的guard与hoistMaybe组合:无 select 权限的表、无主键的表不会进入联邦 parser 列表。
@key实体的解析与查询构造
convertToApolloFedParserFunc/modifyApolloFedParserFunc将单张表转换为ApolloFederationParserFunction:给定一个ApolloFederationAnyType(含__typename与主键值),它遍历表的主键列,从afPKValues中查找对应键值,用parseScalarValueColumnType将 JSON 值解析为列类型,并为每个主键列构造AEQ NonNullableComparison等值条件;所有主键条件以BoolAnd合并为where表达式,最终生成一个QDBSingleRow的单行查询(IR.AnnSelectG),等价于一次按主键的*ByPk查询。同时会应用该列在 select 权限中的 redaction 表达式(getRedactionExprForColumn),保证联邦查询不绕过列级脱敏。
_entities的求值流程
RFC 以如下查询为例说明_entities的求值过程:
query MyQuery { _entities(representations: [{"__typename": "UsersData", "id": "1"}, {"__typename": "TwoPks", "id1": "1", "id2": "2"}]) { ... on TwoPks { internalData } ... on UsersData { id name } } }求值分为四步:
- 取 selection set:从
_entities查询的 selection set 中,按 union 成员类型(上例为TwoPks与UsersData)分别提取 selection set; - 生成参数:用查询参数构造各类型的参数,例如
TwoPks对应(id1: "1", id2: "2"); - 复用
ByPk解析器求值:用各类型的*ByPk字段解析器(TwoPksByPk、UsersDataByPk)对 selection set 与参数构造出的 Field 求值; - 汇总结果:将所有结果拼接为一个列表。
当前源码中的mkEntityUnionFieldParser(见 ApolloFederation.hs)即按此流程实现:对每个 representation,按afTypename在 union 解析结果中查找对应 parser,找不到时报错("<typename>is not found in selection set or apollo federation is not enabled for the type");找到后调用aafuGetRootField生成QueryRootField,最后用concatQueryRootFields = RFMulti将多个根字段合并为一个多字段查询执行。
RFC 同时注明:上述求值方式可能对同一数据库执行多次取数(每个 representation 一次),这是 v1 的已知优化空间。
根字段暴露的完整逻辑
apolloRootFields(见 ApolloFederation.hs)决定最终暴露哪些联邦字段:
- 联邦启用且存在带
@key的表 parser → 同时暴露_service与_entities; - 联邦启用但没有任何联邦表 parser → 只暴露
_service(足以让网关接入,但无实体可被引用); - 未启用联邦 → 不暴露任何联邦字段。
这与 RFC 的两个需求一一对应:_service保证"挂载到网关",_entities保证"其他子图引用表类型"。
端到端验证:测试用例
server/tests-py/queries/apollo_federation/目录下的 YAML 测试用例完整演示了从建表、track 到联邦查询的链路:
setup.yaml中先执行 SQL 建表并插入数据:
CREATE TABLE "user"( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, is_admin BOOLEAN NOT NULL DEFAULT false ); INSERT INTO "user" (id, name, email) VALUES (1, 'foo', 'foo@email.com'), (2, 'bar', 'bar@email.com'), (3, 'bar', 'bar@email.com'), (4, 'baz', 'baz@email.com');随后通过track_table开启联邦:
- type: track_table args: table: user schema: public apollo_federation_config: enable: v1entities.yaml验证_entities查询:以{"__typename": "user", "id": 1}作为 representation,返回id: 1、email: foo@email.com、name: foo、is_admin: false,证明网关按主键id解析实体成功:
query EntitiesTest($representations: [_Any!]!) { _entities(representations: $representations) { ... on user { id email name is_admin } } }root_fields.yaml通过 introspection 断言 query 根字段按预期暴露:_entities(类型为_Entity,UNION类型)、_service(类型为_Service,NON_NULL包裹的OBJECT),以及常规的user列表查询、user_aggregate聚合查询等,验证联邦字段与常规字段共存于根 schema。
限制与未来工作
RFC 在 "Future work" 一节明确列出 v1 之后的演进方向:
- 将 Actions 类型纳入联邦:需要内部表示层面的改动(如在 action 类型中加入 directives 存储),因此 v1 仅覆盖 DB 表与 remote schema 来源;
- 允许用户自定义
@key字段:v1 默认取表主键,未来可让用户选择主键之外的字段作为联邦键; - 评估 Apollo Federation v2 支持:v1 仅实现 v1 规范的
@key最小子集,v2 的@shareable、@link等指令留待后续(源码中已有相关预留注释)。
此外,_entities的多 representation 求值目前会触发多次数据库取数,RFC 明确指出这是后续可优化的性能点。
小结
从 RFC 设计文档 到 ApolloFederation.hs 的实现,HGE 的 Apollo Federation v1 支持形成了一个完整闭环:服务级与表级两级开关控制启用范围,@key指令与_service/_entities联邦字段由 schema 构建期自动生成,_Any解析、按主键构造单行查询与RFMulti合并求值完成实体解析,且全程受 select 权限与列脱敏约束。配合 tests-py 下的联邦测试,开发者可以快速验证"HGE 作为 Apollo 子图被网关消费"的完整链路,为后续向 v2 演进与性能优化打下基础。
【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考