Представление типа варианта enum+object в GraphQL

Существует ли наилучшая практика для представления вариантного поля, которое может быть либо объектом с подполями, либо одним или несколькими одноэлементными значениями, подобными enum? Например, если есть только одно одноэлементное значение, может работать обнуляемый union (хотя это несколько неудобно, если это значение не кажется «нулевым»), но как насчет более чем одного одноэлементного значения?

В моей модели предметной области есть много подобных enum структур, где некоторые варианты не несут никаких данных. Я надеюсь, что ответ не в том, чтобы создавать фиктивные поля, чтобы каждый вариант был типом объекта, чтобы удовлетворить требования union. Но, возможно, я что-то упускаю.


person acjay    schedule 21.12.2017    source источник
comment
проверьте graphene.js. Обеспечить поддержку пользовательских типов перечисления   -  person Deadfish    schedule 22.12.2017


Ответы (3)


В основном это вопрос о том, как моделировать алгебраические типы данных в GraphQL. В частности, как смоделировать совместный продукт, в котором некоторые возможности являются одиночками, а другие являются продуктами. Для тех, кто не знаком с этой терминологией:

продукт – структура данных, объединяющая данные, которые должны отображаться вместе (см. также кортеж, запись, класс данных). , так далее.).

совместный продукт – структура данных, представляющая взаимоисключающие варианты (см. также объединение, либо, тип суммы и т. д. )

singleton — тип, который имеет только один член. Другими словами, он не имеет свободных параметров и не содержит никаких данных, кроме собственного присутствия.

алгебраический тип данных – набор типов произведений и сопутствующих произведений, которые ссылаются друг на друга (возможно, рекурсивно), а также на синглтоны. Это позволяет моделировать произвольно сложные структуры данных.

Мой вывод на данный момент заключается в том, что для этого нет абсолютно чистого и общего ответа. Вот несколько подходов, каждый из которых имеет компромиссы:

Использовать null (ограничено)

Если в вашей структуре данных есть только один необязательный синглтон, вы можете использовать нулевое значение для представления синглтона. См. [Herku's answer][1] для списков. Я включил это для полноты, но оно не распространяется на структуры данных с несколькими синглтонами. И в тех случаях, когда одноэлементный вариант не обязательно представляет отсутствие или пустоту, это может быть неудобно.

Пользовательские скаляры

Если вы хотите отказаться от необходимости проверять внутренние данные в вариантах, у которых есть свойства, вы можете сделать весь вариант непрозрачным пользовательским скаляром в своей схеме:

scalar MyVariant # Could be whatever!

Недостатком является то, что если вы хотите добавить более богатое поведение GraphQL к своим вариантам продукта, вам не повезло. Скаляры — это листовые узлы в дереве запросов.

Одиночный объект в объединении

Вы можете представить тип как union обычных объектов types, а также создать 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, а не для представления данных.

Заключение

Выбрать свой яд. Насколько я знаю, нет отличного ответа на вопрос, как сделать это таким образом, чтобы он был полностью свободен от шаблонного кода или утечки абстракции.

person acjay    schedule 22.12.2017

Я думаю, вы уже дали очень хороший ответ. Я просто хочу добавить к этому еще один подход, который может быть решением в некоторых случаях (в основном, когда у вас есть только небольшое количество типов в сумме). Для этого вы можете просто не использовать систему типов 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, в то время как клиент JavaScript просто использует тип как есть с нулевыми значениями:

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 не очень хорош в запросах рекурсивных структур, и вам, возможно, придется представлять свою структуру по-другому (например, сгладить древовидную структуру).

Дайте мне знать, что вы думаете!

person Herku    schedule 27.12.2017
comment
Я думаю, что это могло бы работать для структуры списка, но я не вижу, как это можно было бы красиво обобщить на более сложные схемы. Как это работает для sealed trait, у которого более одного case object? - person acjay; 28.12.2017

Вы можете создать свой скалярный тип, возможно, как тип строки, и проверить значение перечисления внутри.

person neattom    schedule 23.12.2017
comment
Я думаю, вы в основном предлагаете собственный скалярный подход, который я изложил в своем ответе. - person acjay; 25.12.2017