GraphQL 模式设计中的接口与联合

sdg*_*sdh 6 graphql

假设我正在构建一个提供自然灾害事件时间线服务的 GraphQL API。

现在有两种不同类型的事件:

  • 飓风
  • 地震

所有事件都有一个 ID 和发生日期。我计划使用游标进行分页查询来获取事件。

我可以想到两种不同的方法来建模我的领域。

1. 接口

interface Event {
  id: ID!
  occurred: String! # ISO timestamp
}

type Earthquake implements Event {
  epicenter: String!
  magnitude: Int!
}

type Hurricane implements Event {
  force: Int!
}
Run Code Online (Sandbox Code Playgroud)

2. 联盟

type Earthquake {
  epicenter: String!
  magnitude: Int!
}

type Hurricane {
  force: Int!
}

type EventPayload = 
  | Earthquake
  | Hurricane

type Event {
  id: ID!
  occurred: String! # ISO timestamp
  payload: EventPayload!
}
Run Code Online (Sandbox Code Playgroud)

两种方法之间的权衡是什么?

Jon*_*oux 4

我相信:

  • 联合是关于提供:一个字段/它的解析器函数解析一个对象,该对象的类型属于特定的、已知的类型集。
  • 接口是关于请求的:没有接口,客户端将不得不在每个类型片段中重复他们感兴趣的字段。

它们有不同的用途,并且可以一起使用:

interface I {
    id: ID!
}

type A implements I {
    id: ID!
    a: Int!
}

type B implements I {
    id: ID!
    b: Int!
}

type C implements I {
    id: ID!
    c: Int!
}

union Foo = A | C

type Query {
    foo: Foo!
}
Run Code Online (Sandbox Code Playgroud)

该模式声明AB、 和C有一些共同的字段,以便客户端更容易请求它们,并且查询foo只能产生AC

你能foo: I!代替写一下吗?虽然这可以无缝工作,但我相信这会导致糟糕的开发体验。如果您说foo提供了一个I对象,那么您的客户应该准备好接收任何实现类型,包括B,并且会花时间编写和维护永远不会被调用的代码。如果您知道foo只能产生AC,请明确告诉他们。

foo如果要产生AB、 或 ,同样如此C。碰巧它正是实现的类型列表I,所以在这种情况下,你可以写吗foo: I!?不!别被这个愚弄了。为什么?因为这个列表可以通过联合/模式拼接来扩展!我相信这是某些 GraphQL 框架很少使用的功能,但其采用率正在增长。如果您从未使用过它,请尝试一下,它将让您了解微服务间通信和其他媒体流行语的新想法。简而言之,想象一下您正在制作一个公共 API,甚至在组织内有点公开。其他人可以通过提供额外的东西来“增强”你的 API。这可能包括实现您的接口的新类型。所以我们回到上一段。

到目前为止,看起来我赞成你的第一个代码。

然而,这可能是特定于这种情况的,在我看来,您对事件的定义混合了有关其发生的数据和有关物理指标的数据。您的第二个代码将它们分为两种类型层次结构。我喜欢。感觉对架构更友好。您的架构更加开放。想象一下,您的 API 是关于事件历史记录的,有人通过预测对其进行了增强:您的 APIEventPayload可以重复使用!

此外,请注意您的第一个示例不完整。实现接口的类型必须实现(即重复)该接口的每个字段,就像我在上面的代码中编写的那样。随着字段数量和实现类型数量的增加,这变得更难维护。

所以,第二种解决方案也有一些优点。但这样做的话,我之前提出的有关返回类型具体化的废话很难实现,因为要具体化的有效负载被嵌入到另一种类型中,并且 GraphQL 中不存在泛型之类的东西。

这是协调所有这些的提议:

interface HasForce {
    force: Int!
}

type Earthquake {
    epicenter: String!
    magnitude: Int!
}

type Hurricane implements HasForce {
    force: Int!
}

type Tsunami implements HasForce {
    force: Int!
}

interface Event {
    data: EventData!
}

type EventData {
    id: ID!
    occurred: String!
}

union HistoryMeteorologicalPhenomenon = Earthquake | Hurricane

type HistoryEvent implements Event {
    data: EventData!
    meteorologicalPhenomenon: HistoryMeteorologicalPhenomenon!
}

type Query {
    historyEvents: [HistoryEvent!]!
}
Run Code Online (Sandbox Code Playgroud)

它看起来比你的两个建议更复杂一些,但它满足了我的需求。此外,很少从这个高度来看待模式:更常见的是,我们知道入口点并从那里向下挖掘。例如,我打开 的文档historyEvents,看到它产生两种现象,很好,我不知道存在其他联合类型和事件类型。

如果您要编写大量此类联合+事件对,则可以使用代码生成它们,其中一个函数调用将声明一对。不太容易出错,实施起来更有趣,并且中等文章的潜力更大。

请注意,GraphQL 结构独立于您的存储结构。可以有多个 GraphQL 对象提供来自同一个“在此处插入您的语言”对象的数据,例如由数据库驱动程序生成的数据。可能有一点我没有进行基准测试的开销,但提供一个更干净的 API 对我来说比这更重要。基本思想是解析器函数只需使用相同的源进行解析,以便使用相同的源对象调用与另一个类型相关的解析器函数。