问题描述
我们使用 Apollo Federation 作为我们的主要 API 已经有大约 1.5 年的时间了。联邦网关后面是 6 个子 graphql 服务,它们都在网关处组合。当您拥有跨越不同服务的数据结果集时,此配置确实非常有效。例如。引用购买的用户和与其相关联的事件等的门票列表。
我们遇到过这种故障的一个地方是,需要一组已在另一个子服务(或跨其他子服务)(解析器/路径)中定义的预设数据。没有办法(我们已经发现)从子服务查询联合以获取一组联合数据,以供解析器使用以处理该数据。
例如,假设我们定义了一个 graphql 查询,它查询某个活动的所有门票,并通过联合返回购买者数据、活动数据和产品数据。如果我需要来自解析器的这个数据集,我需要自己再次进行所有这些查询,复制数据源逻辑,并且必须匹配代码中的数据。
出现的一个疯狂想法是设置 apollo-datasource-rest
dataSource 来对我们的网关端点进行查询,作为我们解析器的数据源。通过这种方式,我们可以请求我们需要的数据,并让 Apollo Federation 按照设计的方式将所有数据拼接在一起。因此,与其让解析器查询数据库中所有不同的数据片段然后匹配它们,不如从已经定义了该查询的 graphql 网关请求数据。
这样做是为了避免在子服务中重复查询集,以获取其他服务中(或跨)可用的详细信息。
问题
这真的是个坏主意吗?
这是一个合理的想法吗?
有没有人以前尝试过这样的事情?
是的,我们必须确保解析器没有循环依赖。在我们的例子中,我看到了“访问网关的数据源”用于收集突变中的初始数据。
联合查询的示例。在此查询中,event
、allocatedTo
、purchasedBy
和 product
都是其他服务中的类型。 event
是事件类型,allocatedTo
和 purchasedBy
是配置文件类型,product
是产品类型。这个查询为我提供了所有我会用来说的数据,向结果集中的人发送电子邮件通知。虽然从一个突变中的解析器获取这些数据以对这些电子邮件进行排队意味着我需要进行许多查询并通过自己的代码对齐所有数据,而不是使用网关/联盟,它已经与已经建立的查询一起执行此操作。使用 apollo-datasource-rest
查询我们自己的网关的想法是以这种形式获取这些数据。不是通过单独的查询和代码来对齐 id 等。
query getRegisteredUsers($eventId: ID!) {
communications {
event(eventId: $eventId) {
registered {
event {
name
}
isAllocated,hasCheckedIn,lastUpdatedAt,allocatedTo {
firstName
lastName
email
}
purchasedBy {
id
firstName
lastName
}
product {
__typename
...on Ticket {
id
name
}
}
}
}
}
}
解决方法
仅供参考,直到我查看了您的编辑(其中包含一些示例)之前,我才完全理解这个问题。
这真的是个坏主意吗?
根据我的经验,是的。这不是一个想法,因为您与其他已经这样做的非常聪明的人相处得很好。
这是一个合理的想法吗?
绝对有道理,但我不推荐。
以前有人试过这样的吗?
是的,但我希望你不要。
您的问题
让解析器向网关发送请求:
我不推荐这个。我已经看到这种情况发生,而且我亲自努力帮助公司摆脱这让您陷入困境。循环依赖将会发生。随着您有越来越多的跃点、TLS 握手等,延迟只会飙升。改为进行编排。引入非 GraphQL 感觉很奇怪,但 IMO 最终比“只与网关交谈”更简单、更快、更易于维护。
然后呢?
当您处理一些需要来自多个数据源的数据才能处理单个事情(例如向某人发送交易电子邮件)的突变时,您有一些选择。帮助我解决这个问题的是“在使用 GraphQL 之前我将如何做到这一点?”
-
编排:您有一个“编排服务”,它接受变异并向所有者服务发出调用(最好是非 GraphQL,所以是 REST、gRPC、Lambda?)以收集数据。编排层不拥有数据,但它可以与其他服务对话。它类似于联邦,但用于将数据发送到请求中,而不是发送到响应中。
-
编排:你触发大致相同的事情,但通过事件流。 (不适用于 GraphQL 的请求/响应模型)
-
CQRS(投影):数据库数据的副本,用于报告之类的事情。 CQRS 基本上是“您读取数据的方式不必与您编写它的方式相同”,它允许诸如事件源数据之类的东西。如果您的所有数据源实际上共享同一个数据库,您甚至不需要像只读副本那样多的“投影”。如果您的规模不足以进行复制,请跳过它并保证永远不会写入您当前域不拥有的数据。
我在做什么
在我工作的地方,我让我们:
查询
- 查询总是以“一个数据库调用”开始。
- 如果“一个数据库调用”进入一个数据域(通常为真),则该查询进入一项服务,并且联邦填充树的叶子。如果您真的遵循 CQRS,这可能与 #3 相同,但我们不会。
- 如果您的“一个数据库调用”需要来自跨域的数据(例如,获取包含产品 X 的所有订单,但按客户的名字排序),您需要进行数据库投影。最好这可以由“报告服务”处理:它不拥有任何数据,但它读取所有数据。
突变
- 如果你的顶级变异只在一个域内修改行为,变异进入一个服务,它可以使用数据库事务,联邦填充叶子
- 如果您的变更需要跨多个域写入并且需要立即一致性(下订单时有库存、付款等),我们选择编排来跨多个服务写入(并在必要时回滚,因为我们不有数据库事务来为我们做这件事)。
- 如果您的变更需要来自多个地方的数据进一步发送到请求中(例如发送电子邮件),我们选择编排从多个服务中提取并将该数据下推。这感觉很像联邦,但恰恰相反。