在GraphQL

时间:2017-12-21 22:23:02

标签: enums graphql algebraic-data-types discriminated-union

是否有最佳实践来表示变量字段,该字段可以是具有子字段的对象,也可以是一个或多个enum - 如单例值?就像,如果只有一个单例值,可以为空的union可以工作(如果有点笨拙,如果这个值没有感觉到"零点"),那么更多比一个单身?

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

3 个答案:

答案 0 :(得分:7)

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

产品 - 将必须同时出现的数据组合在一起的数据结构(另请参阅元组记录数据类< / em>等。)。

coproduct - 表示互斥变体的数据结构(另请参阅 union 总和类型等等。)

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

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

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

使用null (有限)

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

自定义标量

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

scalar MyVariant # Could be whatever!

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

联合中的单一对象

您可以将该类型表示为常规对象union的{​​{1}},并将type代表您的单例选项。对于单例,您必须添加某种虚拟字段,因为对象类型必须至少有一个字段。您可以将该字段设为类型名称,但该字段已在type __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中实现是实用的,不一定代表数据。

<强>结论

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

答案 1 :(得分:0)

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

答案 2 :(得分: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
}

这可以在JavaScript中轻松映射回Scala中的和类型 客户端只需使用类型为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不是很擅长查询递归结构,你可能必须以不同的方式表示你的结构(例如,展平你的树结构)。

让我知道你的想法!