界面与GraphQL模式设计中的联合

发布于 2025-02-12 13:03:06 字数 718 浏览 0 评论 0原文

假设我正在建立一个为自然灾害事件的时间表的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!
}

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!
}

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

Suppose I am building a GraphQL API that serves a timeline of natural disaster events.

There are two different kinds of event right now:

  • Hurricane
  • Earthquake

All events have an ID and a date they occurred. I plan to have a paginated query for fetching events using cursors.

I can think of 2 different approaches to modelling my domain.

1. Interface

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

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

type Hurricane implements Event {
  force: Int!
}

2. Union

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

type Hurricane {
  force: Int!
}

type EventPayload = 
  | Earthquake
  | Hurricane

type Event {
  id: ID!
  occurred: String! # ISO timestamp
  payload: EventPayload!
}

What are the trade-offs between the two approaches?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

独享拥抱 2025-02-19 13:03:07

我相信:

  • 工会要提供< / strong>:一个字段 /其分辨率函数与一个对象解析,其类型属于特定的,已知的类型类型。
  • 接口是关于请求:如果没有,客户将不得不重复他们感兴趣的字段,每种类型的片段。

它们具有不同的目的,可以一起使用:

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!
}

此架构声明abc有一些共同的字段,因此客户端更容易请求它们,并且查询foo只能产生ac

您可以编写foo:i!吗?尽管这将无缝起作用,但我相信这会带来不良的发展经历。如果您说的是foo提供i对象,则您的客户应为接收任何实施类型做好准备,包括b和将花时间编写和维护永远不会被调用的代码。如果您知道foo只能产生ac,请明确告诉他们。

如果foo是要产生abc,则相同。碰巧是实现i的类型列表,因此在这种情况下,您可以编写foo:i!吗?不!不要被那个愚弄。为什么?因为此列表是可以通过联邦 /模式缝制的扩展< / strong>!我相信这是某些GraphQl框架的很少使用的功能,但其采用量的增长。如果您从未使用过它,请尝试,它将为您的新想法带来微观服务间交流和其他中等流行语的新想法。简而言之,想象一下您在组织内部进行公共API,甚至在组织内部进行某种公共活动。其他人可以通过提供额外的东西来“增强”您的API。这可能包括实现界面的新类型。因此,我们回到了上一段。

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

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

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

因此,第二个解决方案也具有一些优势。但是这样做,我早些时候对返回类型进行特定的blah-blah很难实现,因为有效载荷(要具体的)嵌入了另一种类型中,并且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!]!
}

看起来这两个建议看起来更复杂,但它可以满足我的需求。另外,很少看这个高度的架构:更多的是,我们经常知道入口点并从那里挖掘。例如,我在historyEvents上打开文档,请参阅它产生两种现象,很好,我不知道存在其他联合类型和事件类型。

如果您要编写很多这些 union + event 对,则可以用代码生成它们,从而使一个函数调用会声明一对。较少的错误,实施更有趣,并且具有更多的中等文章潜力。

请注意,GraphQL结构独立于您的存储结构。可以让多个GraphQl对象从相同的 insert-your语言中提供数据,例如,您的DB驱动程序产生的对象。我可能没有一个小的开销,但我没有标准,但是提供更干净的API对我来说胜过这一点。基本想法是,解析器函数只需要从相同的源解决,因此与同一源对象相关的解析器函数将被调用。

I believe that:

  • unions are about providing: a field / its resolver function resolves with an object, whose type belongs to a specific, known, set of types.
  • interfaces are about requesting: without, the clients would have to repeat the fields they are interested in, in every type fragment.

They serve different purposes, and they can be used together:

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!
}

This schema declares that A, B, and C have some fields in common, so that it's easier for the client to request them, and that querying foo can only yield A or C.

Could you write foo: I! instead? While this would work seamlessly, I believe this leads to a bad development experience. If you're saying that foo provides an I object, your clients should be prepared for receiving any of the implementing types, including B, and would spend time to write and maintain a code that will never be called. If you know that foo can only yield A and C, please tell them explicitly.

The same holds if foo were to yield A, B, or C. It happens that it's exactly the list of types that implement I, so in this case, could you write foo: I!? No! Don't be fooled by that. Why? Because this list is expandable through federation / schema stitching! I believe it's a seldom used feature of some GraphQL frameworks, but whose adoption grows. If you've never used it, please try, it will open your mind to new ideas of inter-micro-service-communication and other Medium buzzwords. In short, imagine you're making a public API, or even somewhat-public within an organization. Someone else could "augment" your API by providing extra stuff. This may include new types implementing your interface. And so we're back to the previous paragraph.

So far, it looks like I'm in favor of your first code.

However, and this may be specific to this scenario, it seems to me that your definition of event mixes both data about its occurrence and about physics metrics. Your second code splits them into two type hierarchy. I like that. It feels more architecture-friendly. Your schema is more open. Imagine your API is about event history, and someone enhance it with forecasts: your EventPayload can be reused!

Besides, note that your first example is incomplete. Types implementing an interface must implement, i.e. repeat, every single field of this interface, like I wrote in the above code. This becomes harder to maintain as the number of fields and the number of implementing types grow.

So, the second solution also has some advantages. But doing so, the blah-blah I made earlier about being specific with returned types is hard to implement, because the payload, which is the one to be specific about, is embedded into another type, and there's no such thing as generics in GraphQL.

Here's a proposal to reconcile all of that:

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!]!
}

It looks a bit more complex that both of your proposals, but it fulfills my needs. Also, it's rare to look at a schema from this height: more often, we know the entry point and dig down from there. For instance, I open the documentation at historyEvents, see that it yields phenomena of two kinds, fine, I'm not aware that other union types and event types exist.

If you were to write a lot of these union + event pairs, you could generate them with code instead, whereby one function call would declare a pair. Less error-prone, funnier to implement, and with more potential of Medium articles.

Note that the GraphQL structure is independent of your storage structure. It's possible to have multiple GraphQL objects providing data from the same insert-your-language-here object, e.g. yielded by your DB driver. There may be a tiny overhead that I haven't benchmarked, but providing a cleaner API outweighs that to me. The basic idea is that resolver functions just have to resolve with the same source, so that the resolver functions related to another type will be called with the same source object.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文