GraphQL Schema Stitching与Apollo Federation在联邦查询中的合并策略
在微服务架构下,将分散在多个服务中的GraphQL Schema合并成一个统一的入口,是后端开发的常见需求。本文将手把手指导你如何使用 Schema Stitching 和 Apollo Federation 这两种主流方案,并重点剖析它们在合并查询时的核心策略。
第一章:理解问题与方案
-
分析:当你的用户数据在一个服务、订单数据在另一个服务时,客户端的一个查询可能需要同时获取这两个服务的数据。如果让客户端分别查询再拼装,效率极低。你需要一个统一的GraphQL网关,它能接收一个复合查询,然后分解并转发给对应的后端服务,最后合并结果返回给客户端。这就是联邦查询。
-
明确:Schema Stitching 和 Apollo Federation 是实现上述网关的两种不同技术路线。前者更像一个“手动”或“工具辅助”的拼接器,后者是一个有明确规范的“自动”联邦框架。
第二章:方案一:Schema Stitching 的工作原理与合并策略
Schema Stitching 的核心思想是:由网关层显式地定义如何将多个远程Schema“缝合”在一起。
-
获取远程 Schema:你的网关程序首先需要从每个下游服务获取其完整的GraphQL Schema定义。
// 假设从`用户服务`和`订单服务`获取到的类型定义片段 // 用户服务 type User { id: ID! name: String orders: [Order] # 这里有一个指向Order类型的字段 } // 订单服务 type Order { id: ID! date: String user: User # 这里有一个指向User类型的字段 } -
定义合并关系:这是最关键的一步。你需要编写代码来告诉网关,当查询
User类型时,如何去获取它的orders字段。// 网关代码示例 (JavaScript) const mergedSchema = stitchSchemas({ subschemas: [ { schema: userSchema, url: 'http://user-service/graphql' }, { schema: orderSchema, url: 'http://order-service/graphql' } ], typeDefs: ` extend type User { orders: [Order] } `, resolvers: { User: { orders: { // 指定合并策略 selectionSet: '{ id }', // 需要User的id来查询订单 resolve(user, args, context, info) { // 手动调用订单服务,用user.id查询其关联订单 return info.mergeInfo.delegateToSchema({ schema: orderSchema, operation: 'query', fieldName: 'ordersByUserId', args: { userId: user.id }, context, info }); } } } } }); -
理解其合并策略:
- 聚合式合并:网关会根据你定义的
resolvers,在运行时手动发起多次查询。对于上面的例子,一个查询{ user { id, name, orders { id } } }会被分解为:- 先查询
用户服务获取id和name。 - 再用获取到的
user.id,调用订单服务的ordersByUserId查询器,获取订单列表。 - 网关将两次查询的结果在内存中拼接成最终对象返回。
- 先查询
- 控制权:开发者完全掌控合并的逻辑,可以编写复杂的转换和拼接代码。
- 缺点:需要编写大量胶水代码,维护成本高。每次Schema变更都可能需要更新合并逻辑。
- 聚合式合并:网关会根据你定义的
第三章:方案二:Apollo Federation 的工作原理与合并策略
Apollo Federation 引入了一套规范的指令(@key, @external, @requires, @provides),让服务能够“声明”自己提供哪些数据以及数据之间的关系。
-
改造下游服务:服务需要声明自己拥有某个类型的一部分。例如,订单服务声明自己能提供
Order类型,并且知道Order关联的User的id。# 订单服务 (order-service) type Order @key(fields: "id") { id: ID! date: String user: User } type User @key(fields: "id") { id: ID! # @external 表示这个字段来自用户服务,这里只是引用 }用户服务也需要类似声明。
# 用户服务 (user-service) type User @key(fields: "id") { id: ID! name: String orders: [Order] # 声明自己可以扩展出orders字段 } type Order @key(fields: "id") { id: ID! # @external } -
自动生成网关:使用
@apollo/gateway,它会自动发现所有联邦服务,并根据服务声明的指令,推导出实体之间的关系。const gateway = new ApolloGateway({ serviceList: [ { name: 'user-service', url: 'http://localhost:4001' }, { name: 'order-service', url: 'http://localhost:4002' } ] }); -
理解其合并策略:
- 声明式与自动解析:开发者通过指令声明意图,网关自动生成一个执行计划(Query Plan)。这个计划描述了如何将一个复杂的查询分解为一系列顺序或并行的步骤。
- 实体与引用:核心概念是
Entity(用@key指令声明的类型)。网关通过实体的唯一标识(如id)来追踪和合并数据。 - 执行过程:对于同样的查询
{ user { id, name, orders { id } } },网关可能:- 先查询用户服务,获取
User的基本信息。 - 网关知道
Order是另一个实体,于是会用获取到的User对象,查询订单服务的_entities解析器,要求返回与该User关联的Order对象列表。这个过程对开发者是透明的。
- 先查询用户服务,获取
- 优势:极大减少了样板代码。服务自治性强,可以独立演进。查询计划更智能,可能进行优化(如批处理多个实体查询)。
第四章:核心策略对比与选择指南
下表总结了两种方案在关键维度上的差异:
| 对比维度 | Schema Stitching | Apollo Federation |
|---|---|---|
| 核心思想 | 编程式缝合,显式定义关系 | 声明式联邦,规范定义实体 |
| 合并控制 | 开发者完全控制每个字段的合并逻辑 | 由网关根据规范自动推导执行计划 |
| 关联实现 | 需在网关层编写 resolvers |
服务端用 @key 等指令声明,网关自动生成 _entities 查询 |
| 性能与优化 | 优化取决于开发者代码 | 网关可自动进行查询计划优化,如批量解析 |
| 类型安全 | 在合并时手动保证 | 由规范和网关工具链辅助保证 |
| 适用场景 | 合并逻辑复杂、需精细控制、或集成第三方GraphQL API | 微服务团队协作、追求开发效率、需要独立部署的服务 |
-
选择 Schema Stitching 当:
- 你需要合并的是由第三方提供的、无法修改的GraphQL API。
- 你的合并逻辑异常复杂,超出了标准声明(如
@key)所能表达的范围。 - 你的团队对GraphQL内部机制非常熟悉,希望拥有绝对的控制权。
-
选择 Apollo Federation 当:
- 你正在构建全新的、由多个自主团队维护的微服务架构。
- 你希望标准化服务间的契约,并最小化网关层的代码量。
- 你需要利用成熟的生态系统(如 Apollo Studio)进行监控和性能分析。
第五章:实施步骤(以Apollo Federation为例)
假设你已有一个用户服务和一个订单服务。
-
安装联邦依赖:在每个下游服务项目中,安装
@apollo/subgraph包。npm install @apollo/subgraph -
装饰Schema:使用指令改造服务的类型定义。
# user-service/schema.graphql type User @key(fields: "id") { id: ID! name: String }# order-service/schema.graphql type Order @key(fields: "id") { id: ID! date: String user: User! } extend type User @key(fields: "id") { id: ID! @external orders: [Order] } -
暴露
_entities解析器:在服务的入口文件中,将ApolloServer配置为子图服务。const server = new ApolloServer({ schema: buildSubgraphSchema({ typeDefs }) }); -
搭建网关:新建一个网关项目,安装
@apollo/gateway。const { ApolloServer } = require('apollo-server'); const { ApolloGateway } = require('@apollo/gateway'); const gateway = new ApolloGateway({ serviceList: [ { name: 'user', url: 'http://localhost:4001' }, { name: 'order', url: 'http://localhost:4002' } ] }); const server = new ApolloServer({ gateway }); server.listen().then(({ url }) => console.log(`Gateway ready at ${url}`)); -
启动与验证:依次启动用户服务、订单服务和网关。使用客户端向网关发送一个复合查询,验证数据是否被正确地跨服务合并返回。
第六章:从Stitching迁移到Federation
如果你的系统已基于Schema Stitching构建,迁移到Federation是一个渐进过程。
- 识别边界:将现有的单体Schema或 Stitching 逻辑,按领域上下文拆分成多个独立的服务。
- 逐个改造服务:选择一个服务,为其Schema添加
@key等指令,并暴露为联邦子图。 - 切换网关:将新改造的服务从旧的 Stitching 网关配置中移除,并添加到新的 Federation 网关的服务列表中。逐步将流量切到新网关。
- 清理代码:当所有服务都迁移完成后,下线旧的 Stitching 网关和相关的合并代码。

暂无评论,快来抢沙发吧!