像这样的 graphql 架构:
type User {
id: ID!
location: Location
}
type Location {
id: ID!
user: User
}
Run Code Online (Sandbox Code Playgroud)
现在,客户端发送一个graphql
查询。理论上,User
和Location
可以无限循环引用对方。
我认为这是一种反模式。据我所知,没有中间件或方法可以限制查询的嵌套深度graphql
和apollo
社区。
这种无限嵌套深度查询会为我的系统消耗大量资源,例如带宽、硬件、性能。不仅是服务器端,还有客户端。
所以,如果graphql模式允许循环引用,应该有一些中间件或方法来限制查询的嵌套深度。或者,为查询添加一些约束。
也许不允许循环引用是一个更好的主意?
我更喜欢发送另一个查询并在一个查询中执行多个操作。这要简单得多。
更新
我找到了这个库:https : //github.com/slicknode/graphql-query-complexity。如果 graphql 不限制循环引用。该库可以保护您的应用程序免受资源耗尽和 DoS 攻击。
Dan*_*den 12
这取决于。
记住相同的解决方案在某些情况下可能是一种很好的模式,而在其他情况下可能是一种反模式,记住这一点很有用。解决方案的价值取决于您使用它的上下文。—马丁·福勒
循环引用会带来额外的挑战,这是一个有效的观点。正如您所指出的,它们是潜在的安全风险,因为它们使恶意用户能够制作可能非常昂贵的查询。根据我的经验,它们还使客户团队更容易无意中过度获取数据。
另一方面,循环引用允许更高级别的灵活性。运行您的示例,如果我们假设以下架构:
type Query {
user(id: ID): User
location(id: ID): Location
}
type User {
id: ID!
location: Location
}
type Location {
id: ID!
user: User
}
Run Code Online (Sandbox Code Playgroud)
很明显,我们可能会进行两个不同的查询来有效地获取相同的数据:
{
# query 1
user(id: ID) {
id
location {
id
}
}
# query 2
location(id: ID) {
id
user {
id
}
}
}
Run Code Online (Sandbox Code Playgroud)
如果您的 API 的主要使用者是一个或多个在同一项目上工作的客户团队,那么这可能无关紧要。您的前端需要它获取的数据具有特定形状,您可以围绕这些需求设计架构。如果客户端总是获取用户,可以通过这种方式获取位置并且不需要该上下文之外的位置信息,那么只进行user
查询并user
从Location
类型中省略该字段可能是有意义的。即使您需要location
查询user
,根据客户的需要,在其上公开字段可能仍然没有意义。
另一方面,假设您的 API 被大量客户端使用。也许您支持多个平台,或者多个执行不同操作但共享相同 API 来访问您的数据层的应用程序。或者,您可能正在公开一个旨在让第三方应用程序与您的服务或产品集成的公共 API。在这些情况下,您对客户需求的想法更加模糊。突然之间,公开各种查询底层数据的方法以满足当前客户和未来客户的需求变得更加重要。对于单个客户的 API 来说也是如此,其需求可能会随着时间的推移而发展。
始终可以按照您的建议“展平”您的架构,并提供额外的查询,而不是实现关系字段。但是,这样做对客户端来说是否“更简单”取决于客户端。最好的方法可能是让每个客户选择适合他们需要的数据结构。
与大多数架构决策一样,需要权衡取舍,适合您的解决方案可能与适合其他团队的解决方案不同。
如果您确实有循环引用,则不会失去所有希望。一些实现具有用于限制查询深度的内置控件。GraphQL.js 没有,但是有像graphql-depth-limit这样的库可以做到这一点。值得指出的是,广度可能与深度一样大——无论您是否有循环引用,您都应该在解析列表时考虑使用最大限制实现分页,以防止客户端可能请求一次有数千条记录。
正如@DavidMaze 指出的那样,除了限制客户端查询的深度之外,您还可以使用dataloader
来降低从数据层重复获取相同记录的成本。虽然dataloader
通常用于批处理请求以解决由延迟加载关联引起的“n+1 问题”,但它也可以在这里提供帮助。除了批处理之外,dataloader 还会缓存加载的记录。这意味着同一记录(在同一请求内)的后续加载不会命中数据库,而是从内存中获取。
TLDR;循环引用是非速率限制 GraphQL API 的反模式。具有速率限制的 API 可以安全地使用它们。
长答案:是的,真正的循环引用是更小/更简单的 API 的反模式……但是当您达到对 API 进行速率限制的地步时,您可以使用该限制来“用一块石头杀死两只鸟”。
其他答案之一给出了一个完美的例子:Github 的 GraphQL API 让你请求一个存储库,它的所有者,他们的存储库,他们的所有者......无限......或者你可能会从架构中思考。
如果您查看 API ( https://developer.github.com/v4/object/user/ ),您会发现它们的结构不是直接循环的:中间有类型。例如,User
不引用Repository
,它引用RepositoryConnection
。现在,RepositoryConnection
确实有一个RepositoryEdge
,它确实有一个nodes
类型的属性[Repository]
...
...但是当你查看API的实现时:https : //developer.github.com/v4/guides/resource-limitations/你会看到类型背后的解析器是速率限制的(即没有每个查询超过 X 个节点)。这可以防止请求过多的消费者(基于广度的问题)和无限请求的消费者(基于深度的问题)。
每当用户在 GitHub 上请求资源时,它都可以允许循环引用,因为它不会让它们循环到消费者身上。如果消费者失败,则查询会因速率限制而失败。
这让负责任的用户可以询问由同一用户拥有的存储库的用户......如果他们真的需要......只要他们不继续询问该存储库的所有者拥有的存储库,拥有经过 ...
因此,GraphQL API 有两个选择:
如果您不想限制速率,GraphQL 使用不同类型的方法仍然可以为您提供解决方案的线索。
假设您有用户和存储库:两者都需要两种类型,一个 User 和 UserLink(或 UserEdge、UserConnection、UserSummary ...任您选择),以及一个 Repository 和 RepositoryLink。
每当有人通过根查询请求用户时,您都会返回 User 类型。但该用户类型不会有:
repositories: [Repository]
Run Code Online (Sandbox Code Playgroud)
它会有:
repositories: [RepositoryLink]
Run Code Online (Sandbox Code Playgroud)
RepositoryLink
将具有与 Repository 相同的“平面”字段,但没有其潜在的圆形对象字段。相反owner: User
,它会有owner: ID
。
您展示的模式对于“图形”来说是相当自然的,我认为在 GraphQL 中并不是特别不鼓励这样做。在GitHub的GraphQL API是物我经常看的时候我不知道“人们如何构建更大GraphQL的API”,并且有定期对象周期有:一库有RepositoryOwner,它可以是一个用户,其中有一个列表repositories
。
至少graphql-ruby 有一个控制来限制嵌套深度。Apollo 显然没有这种控制,但您可以构建自定义数据源或使用DataLoader库来避免重复获取您已有的对象。