graphql模式循环引用是反模式吗?

时间:2018-12-20 07:10:12

标签: graphql apollo

graphql模式如下:

type User {
  id: ID!
  location: Location
}

type Location {
  id: ID!
  user: User
}

现在,客户端发送一个graphql查询。从理论上讲,UserLocation可以无限循环地相互引用。

我认为这是一种反模式。据我所知,在graphqlapollo社区中,没有中间件或方法来限制查询的嵌套深度。

此无限嵌套深度查询将为我的系统消耗大量资源,例如带宽,硬件,性能。不仅在服务器端,而且在客户端。

因此,如果graphql模式允许循环引用,则应该有一些中间件或方法来限制查询的嵌套深度。或者,为查询添加一些约束。

也许不允许循环引用是一个更好的主意?

我更喜欢发送另一个查询并在一个查询中执行多项操作。这要简单得多。

4 个答案:

答案 0 :(得分:4)

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。

无论何时有人通过根查询请求用户,您都将返回用户类型。但是该用户类型将具有:

repositories: [Repository]

它将具有:

repositories: [RepositoryLink]

RepositoryLink将具有与存储库相同的“平面”字段,但没有一个潜在的圆形对象字段。而不是owner: User,它将有owner: ID

答案 1 :(得分:2)

要视情况而定。

  

请记住,相同的解决方案在某些情况下可能是一个好的模式,而在另一些情况下可能是一个反模式,这很有用。解决方案的价值取决于您使用它的上下文。 — 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查询并从user类型。即使您需要一个Location查询,根据客户的需求,在其上公开一个location字段仍然可能没有任何意义。

另一方面,假设您的API被大量客户端占用。也许您支持多个平台,或者多个执行不同功能但共享相同API来访问数据层的应用程序。或者,也许您正在公开一个旨在使第三方应用程序与您的服务或产品集成的公共API。在这些情况下,您对客户需求的想法非常模糊。突然之间,公开多种查询基础数据的方法以满足当前客户和未来客户的需求变得更加重要。对于需要随时间变化的单个客户端的API来说也是如此。

始终可以按照您的建议“展平”架构并提供其他查询,而不是实现关系字段。但是,对于客户端而言,这样做是否“简单”取决于客户端。最好的方法可能是使每个客户端都能选择适合其需求的数据结构。

与大多数架构决策一样,需要权衡取舍,对于您来说正确的解决方案可能与其他团队的解决方案不同。

如果您具有循环引用,则所有希望都不会丢失。一些实现具有用于限制查询深度的内置控件。 GraphQL.js没有,但是像graphql-depth-limit这样的库就可以做到这一点。值得指出的是,宽度 depth 一样是一个大问题-不管您是否有循环引用,都应考虑使用以下方法实现分页解析列表时的最大限制也可以防止客户端一次请求数千条记录。

正如@DavidMaze指出的那样,除了限制客户端查询的深度之外,您还可以使用user来减轻从数据层重复获取同一记录的成本。尽管dataloader通常用于批处理请求,以解决由于延迟加载关联而引起的“ n + 1问题”,但它在这里也可以提供帮助。除了批处理之外,数据加载器还缓存加载的记录。这意味着同一记录(在同一请求内)的后续加载不会到达数据库,而是从内存中获取。

答案 2 :(得分:1)

您显示的模式对于“图形”来说是很自然的,我认为在GraphQL中不特别建议不要使用它。当我想知道“人们如何构建更大的GraphQL API”时,我经常会看到GitHub GraphQL API,并且那里经常有对象循环:Repository有一个RepositoryOwner,它可以是User,其中包含repositories的列表。

至少graphql-ruby has a control to limit nesting depth。 Apollo显然没有此控件,但是您可以构建自定义data source或使用DataLoader库来避免重复获取已经拥有的对象。

答案 3 :(得分:1)

以上答案为这个问题提供了很好的理论讨论。我想添加更多软件开发中出现的实际考虑。

正如@daniel-rearden 指出的,循环引用的结果是它允许多个查询文档检索相同的数据。根据我的经验,这是一个不好的做法,因为它使 GraphQL 请求的客户端缓存更难预测,更困难,因为开发人员必须明确指定文档以不同的结构返回相同的数据。

此外,在单元测试中,很难为其字段/属性包含对父级的循环引用的对象生成模拟数据。 (至少在 JS/TS 中;如果有支持这种轻松开箱即用的语言,我很乐意在评论中听到)

维护清晰的数据层次结构似乎是可理解和可维护模式的明确选择。如果经常需要对字段父项的引用,最好构建一个单独的查询。

旁白:说实话,如果不是因为循环引用的实际后果,我很想使用它们。将数据结构表示为“数学上完美的”有向图会很漂亮也很神奇。