视情况而定。
请记住,相同的解决方案在某些情况下可能是好的模式,而在其他情况下可能是反模式。解决方案的价值取决于您使用它的环境。 ——Martin Fowler
循环引用可能会带来额外的挑战,这是一个有效的观点。正如您所指出的,它们是一个潜在的安全风险,因为它们使恶意用户能够制作可能非常昂贵的查询。根据我的经验,它们还使客户团队更容易无意中过度获取数据。
另一方面,循环引用提供了更高级别的灵活性。如果我们假设以下架构,则使用您的示例运行:
type Query {
user(id: ID): User
location(id: ID): Location
}
type User {
id: ID!
location: Location
}
type Location {
id: ID!
user: User
}
很明显,我们可能会进行两个不同的查询来有效地获取相同的数据:
{
# query 1
user(id: ID) {
id
location {
id
}
}
# query 2
location(id: ID) {
id
user {
id
}
}
}
如果您的 API 的主要使用者是一个或多个致力于同一个项目的客户团队,这可能无关紧要。您的前端需要它获取的数据具有特定的形状,并且您可以围绕这些需求设计您的架构。如果客户端总是获取用户,可以通过这种方式获取位置并且不需要该上下文之外的位置信息,那么只有一个 user 查询并从 Location 类型中省略 user 字段可能是有意义的.即使您需要 location 查询,根据客户的需要,在其上公开 user 字段可能仍然没有意义。
另一方面,假设您的 API 被大量客户端使用。也许你支持多个平台,或者多个应用程序做不同的事情但共享相同的 API 来访问你的数据层。或者,您可能正在公开一个旨在让第三方应用程序与您的服务或产品集成的公共 API。在这些情况下,您对客户需求的概念会更加模糊。突然间,公开各种查询基础数据的方法以满足当前客户和未来客户的需求变得更加重要。对于单个客户端的 API 也可以这样说,其需求可能会随着时间的推移而变化。
始终可以按照您的建议“扁平化”您的架构并提供额外的查询,而不是实现关系字段。但是,这样做对客户来说是否“更简单”取决于客户。最好的方法可能是让每个客户选择适合他们需要的数据结构。
与大多数架构决策一样,需要权衡取舍,适合您的解决方案可能与其他团队不同。
如果您确实有循环引用,所有希望都不会丢失。一些实现具有用于限制查询深度的内置控件。 GraphQL.js 没有,但是有像 graphql-depth-limit 这样的库可以做到这一点。值得指出的是,breadth 可能与 depth 一样大 - 无论您是否有循环引用,您都应该考虑使用解析列表时的最大限制,以防止客户端可能一次请求数千条记录。
正如@DavidMaze 指出的那样,除了限制客户端查询的深度之外,您还可以使用dataloader 来降低从数据层重复获取相同记录的成本。虽然dataloader 通常用于批处理请求以解决因延迟加载关联而引起的“n+1 问题”,但它在这里也可以提供帮助。除了批处理,dataloader 还会缓存加载的记录。这意味着同一记录(在同一请求内)的后续加载不会访问数据库,而是从内存中获取。