在 GraphQL 中表示枚举+对象变体类型



是否有表示变体字段的最佳实践,该变量字段可以是具有子字段的对象,也可以是一个或多个类似enum单例值的对象?比如,如果只有一个单例值,一个可为空的union可以工作(如果有点尴尬,如果该值感觉不"零"),但是多个单例呢?

我的领域模型有很多像这样的enum结构,其中一些变体不携带任何数据。我希望答案不是制作虚拟字段,以便每个变体都是对象类型,以满足union的要求。但也许我错过了一些东西。

这基本上是一个如何在 GraphQL 中对代数数据类型进行建模的问题。特别是,如何对联产品进行建模,其中某些可能性是单例,而其他可能性是乘积。对于那些不熟悉此术语的人:

product- 一种数据结构,它将必须一起显示的数据组合在一起(另请参阅元组记录数据类等)。

共积- 表示互斥变体的数据结构(另请参阅并集两者、总和类型等)

单一实例- 只有一个成员的类型。换句话说,它没有自由参数,除了它自己的存在之外,不包含任何数据。

代数数据类型- 一组相互引用(可能递归)以及单例的乘积和共积类型。这允许对任意复杂的数据结构进行建模。

到目前为止,我的结论是,对此没有完全干净和通用的答案。以下是一些方法,所有这些方法都有权衡:

使用null (有限)

如果数据结构只有一个非可选的单一实例,则可以使用可为空性来表示单一实例。有关列表,请参阅[Herku's answer][1]。为了完整起见,我将其包括在内,但它不能推广到具有多个单例的数据结构。在单例变体不一定代表缺席或空虚的情况下,它可能会很尴尬。

自定义标量

如果您愿意放弃检查具有属性的变体中的内部数据的需要,则可以将整个变体设置为架构中的不透明自定义标量:

scalar MyVariant # Could be whatever!

缺点是,如果你想在你的产品变体上添加更丰富的 GraphQL 行为,你就不走运了。标量是查询树中的叶节点。

联合中的单例对象

可以将类型表示为常规对象types 的union,也可以将type表示单例选项。对于单例,您必须添加某种虚拟字段,因为对象类型必须至少有一个字段。您可以将该字段设置为类型名称,但这已经作为每个type__typename提供。我们选择只使它的值始终为空null

union MyVariant = RealObject | Singleton1 | Singleton2
type RealObject {
field1: String
field2: Int
}
type Singleton1 {
singletonDummyField: String # ALWAYS `null`
}
type Singleton2 {
singletonDummyField: String # ALWAYS `null`  
}
type Query {
data: MyVariant
}
# query looks like:
{
data {
myVariantType: __typename
... on RealObject {
field1
}
}
}

因此,对__typename的查询满足在对象类型的查询中至少提供一个字段的需求。你永远不会查询singletonDummyField,而且你几乎可以忘记它的存在。

在GraphQL 服务器中创建一个帮程序很容易,它将为您具体化单例类型 - 它们唯一的变体是它们的名称和元数据。缺点是客户端查询会获得样板。

单例对象嵌入接口

如果虚拟字段的想法令人反感,并且您希望创建自己的类型字段,则可以创建一个显式具有类型字段的interface,并使用enum来表示类型。所以:

enum MyVariantType {
REAL_OBJECT
SINGLETON1
SINGLETON2
}
interface MyVariant {
myVariantType: MyVariantType
}
type RealObject implements MyVariant {
myVariantType: MyVariantType
field1: String
field2: Int
}
type Singleton1 implements MyVariant {
myVariantType: MyVariantType
}
type Singleton2 implements MyVariant {
myVariantType: MyVariantType
}
type Query {
data: MyVariant
}
# query looks like:
{
data {
myVariantType
... on RealObject {
field1
}
}
}

因此,在这里,您查询"真正的"非元myVariantType字段,并且单例类型具有"真实"字段,即使它们相对于__typename字段是多余的。当然,您仍然可以自由使用__typename方法,但有什么意义。这里的缺点是有更多的服务器端样板来实现该模式。但这也可能被考虑在帮助程序中,只是更复杂一些。

组成

您可以将单例编码为包含在对象类型中的enum,该对象类型仅用于包含它们。

union MyVariant = RealObject | Singleton
type RealObject {
field1: String
field2: Int
}
type Singleton {
variation: SingletonVariation!
}
enum SingletonVariation {
SINGLETON1
SINGLETON2
}
type Query {
data: MyVariant
}
# query looks like:
{
data {
... on RealObject {
field1
}
... on Singleton {
variation
}
}
}

这样做的好处是不诉诸内省或冗余字段。这里的缺点是,它将单例变体与产品变体分开分组在一起,可能没有意义。换句话说,模式的结构对于在 GraphQL 中实现是务实的,不一定代表数据。

结论

选择你的毒药。据我所知,对于如何以一种完全没有样板或抽象泄漏的方式做到这一点,没有很好的答案。

我想你已经提供了一个很好的答案。我只想补充另一种方法,在某些情况下可以成为解决方案(主要是当您的总和中只有少量类型时)。为此,您可能只想根本不使用 GraphQL 类型系统来镜像类型。不使用接口会使查询不那么冗长,并将结果的映射留给客户端实现。相反,您可以简单地使用可为空的字段。

示例:整数链表

sealed trait List
case class Cons(value: Int, next: List) extends List
case object Nil extends List

您可以使用可为空的字段为此创建单个类型:

type List {
value: Int
next: List
}

这可以很容易地映射回 Scala 中的 sum 类型,而 JavaScript 客户端只需按原样使用类型,并放置 null:

def unwrap(t: Option[GraphQLListType]) = t match {
case Some(GraphQLListType(value, next))) => Cons(value, unwrap(next))
case None => Nil
}

这使得查询不那么冗长,并且类型定义仅包含一个类型而不是三个类型。

{
list {
value
next
}
# vs.
list {
__typename
... on Cons {
value
next
}
}
}

您还可以轻松地向类型添加更多字段。

不幸的是,GraphQL 不太擅长查询递归结构,你可能不得不以不同的方式表示你的结构(例如,扁平化你的树结构)。

让我知道你的想法!

您可以创建标量类型(可能为字符串类型),并在其中验证枚举值。

最新更新