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

问题描述 投票:1回答:3

是否有最佳实践来表示变量字段,该变量字段可以是具有子字段的对象,也可以是一个或多个enum样单例值?就像,如果只有一个单例值,可以使用可空的union(如果有点笨拙的话,如果这个值感觉不到“零”),那么多个单例呢?

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

enums graphql algebraic-data-types discriminated-union
3个回答
7
投票

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

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

coproduct - 表示互斥变化的数据结构(另请参见union,sum,sum类型等)

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

代数数据类型 - 一组产品和联产品类型,它们相互引用(可能是递归的),也适用于单例。这允许对任意复杂的数据结构进行建模。

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

使用null(有限)

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

自定义标量

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

scalar MyVariant # Could be whatever!

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

联合中的Singleton对象

您可以将该类型表示为常规对象unions的type,并使types代表您的单例选项。对于单例,您必须添加某种虚拟字段,因为对象类型必须至少有一个字段。您可以将该字段设置为类型名称,但在每个__typename上已经可以使用type。我们选择让它成为可以为空的,其值始终是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服务器中创建一个帮助器,为您实现单件类型 - 它们唯一的变体是它们的名称和元数据。缺点是客户端查询获得样板。

Singleton对象实现接口

如果虚拟字段的想法令人反感,并且您希望创建自己的类型字段,则可以创建明确具有类型字段的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中实现是实用的,不一定代表数据。

结论

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


0
投票

您可以创建标量类型,可能是字符串类型,并验证内部的枚举值。


0
投票

我想你已经提供了一个非常好的答案。我只想添加另一种方法,在某些情况下可以成为一种解决方案(大多数情况下,你只有少量类型)。为此,您可能根本不想使用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不是很擅长查询递归结构,你可能必须以不同的方式表示你的结构(例如,展平你的树结构)。

让我知道你的想法!

© www.soinside.com 2019 - 2024. All rights reserved.