В последнее время GraphQL набирает всё большую популярность. Изящный синтаксис запросов, типизация и подписки.


Кажется: "вот оно — мы нашли идеальный язык обмена данными!"...


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


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


Думаю, что стоит сделать небольшую ремарку относительно того, где я применяю данный язык. Это довольно сложная SPA-админка, большая часть операций в которой — это довольно нетривиальный CRUD (сложновложенные сущности). Значительная часть аргументации в данном материале связана именно с характером приложения и характером обрабатываемых данных. В приложениях другого типа (или с другим характером данных) таких проблем может и не возникнуть в принципе.

1. NON_NULL


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


Есть в функциональных (и не только) языках программирования, такая парадигма — монады. Так вот, есть там такая штука, как монада Maybe (Haskel) или Option(Scala), Суть в том, что содержащееся внутри такой монады значение, может существовать, а может и не существовать (то есть быть null'ом). Ну или это может быть реализовано через enum, как в Rust'е.


Так или иначе, а в большинстве языков это значение, которое "оборачивает" исходное, делает null дополнительным вариантом к основному. Да и синтаксически — это всегда дополнение к основному типу. Это не всегда именно отдельный класс типа — в некоторых языках это просто дополнение в виде суффикса или префикса ?.


В GraqhQL всё наоборот. Все типы по умолчанию nullable — и это не просто пометка типа как nullable, это именно монада Maybe наоборот.


И если мы рассмотрим участок интроспекции поля name для вот такой схемы:


# в примерах далее я буду опускать schema - будем считать, что это очевидно
schema {
  query: Query
}

type Query {
   # здесь восклицательный знак как раз обозначает NonNull
   name: String! 
}

то обнаружим:


image


Тип String обернут в NON_NULL


1.1. OUTPUT


Почему именно так? Если коротко — это связано, с "толерантным" по умолчанию дизайном языка (в числе прочего — дружелюбным к микросервисной архитектуре).


Чтобы понять суть этой "толерантности", рассмотрим чуть более сложный пример, в котором все возвращаемые значения строго обернуты в NON_NULL:


type User {
   name: String!
   # Обращаем внимание: это ненулевое поле содержащее колекцию ненулевых пользователей.
   friends: [User!]! 
}

type Query {
   # Обращаем внимание: это ненулевое поле содержащее колекцию ненулевых пользователей.
   users(ids: [ID!]!): [User!]!
}

Предположим, что у нас есть сервис, возвращающий список пользователей, и отдельный микро-сервис "дружбы", который возвращает нам сопоставление для друзей пользователя. Тогда, в случае отказа сервиса "дружбы", мы вообще не сможем вывести список пользователей. Нужно исправить ситуацию:


type User {
   name: String!
   # Убрали восклицательный знак - допускаем null вместо списка друзей.
   # Теперь если сервис "дружбы" упадет - мы всё равно сможем вернуть пользователя, хотябы и без друзей.
   friends: [User!] 
}

Вот это и есть толерантность к внутренним ошибкам. Пример, конечно, надуманный. Но надеюсь, что суть вы ухватили.


Кроме того, можно немного облегчить себе жизнь в других ситуациях. Предположим, что есть удаленные пользователи, а айдишники друзей могут хранится в какой-то внешней не связанной структуре. Мы могли бы просто отсеять и вернуть только то, что есть, но тогда мы не сможем понять что именно было отсеянно.


type Query {
   # Допускаем null в списке пользователей. 
   # Теперь мы сможем сопоставить коллекцию идентификаторов с коллекцией пользователей по индексам и понять какие айдишники устарели.
   users(ids: [ID!]!): [User]!
}

Всё ок. А в чем проблема-то?
В общем, не очень большая проблема — так вкусовщина. Но если у вас монолитное приложение с реляционной бд, то скорее всего ошибки — это действительно ошибки, а апи должно быть максимально строгим. Здравствуйте, восклицательные знаки! Везде, где можно.


Я бы хотел иметь возможность "инвертировать" это поведение, и расставлять вопросительные знаки, вместо восклицательных ) Привычнее было бы как-то.


1.2. INPUT


А вот при вводе, nullable — это вообще отдельная история. Это косяк уровня checkbox в HTML (думаю, что все помнят эту неочевидность, когда поле неотмеченного чекбокса просто не отправляется на бэк).


Рассмотрим пример:


type Post {
  id: ID!
  title: String!
  # Обращаем внимание: поле описания может содержать null
  description: String
  content: String!
}

input PostInput {
  title: String!
  # Обращаем внимание: поле описания не является обязательным, для ввода
  description: String
  content: String!
}

type Mutation {
  createPost(post: PostInput!): Post!
}

Пока всё нормально. Добавим update:


type Mutation {
  createPost(post: PostInput!): Post!
  updatePost(id: ID!, post: PostInput!): Post!
}

А теперь вопрос: что нам ожидать от поля description при апдейте поста? Поле может быть null, а может вообще отсутствовать.


Если поле отсутствует, то что нужно сделать? Не обновлять его? Или установить его в null? Суть в том, что разрешить значение null и разрешить отсутствие поля — это разные вещи. Тем не менее в GraphQL — это одно и тоже.


2. Разделение ввода и вывода


Это просто боль. В модели работы CRUD, ты получаешь объект с бэка "подкручиваешь" его, и отправляешь назад. Грубо говоря, это один и тот же объект. Но тебе просто придется описать его дважды — на ввод и на вывод. И с этим ничего нельзя сделать, кроме как написать генератор кода под это дело. Я бы предпочел разделять на "вводимы и выводимые" не сами объекты, а поля объекта. Например модификаторами:


type Post {
  input output text: String!
  output updatedAt(format: DateFormat = W3C): Date!
}

или используя директивы:


type Post {
  text: String!
  @input @output

  updatedAt(format: DateFormat = W3C): Date!
  @output
}

3. Полиморфизм


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


interface Commentable {
  comments: [Comment!]!
}

type Post implements Commentable {
  text: String!
  comments: [Comment!]!
}

type Photo implements Commentable {
  src: URL!
  comments: [Comment!]!
}

или юнионы


type Person {
  firstName: String,
  lastName: String,
}

type Organiation  {
  title: String
}

union Subject = Organiation | Person

type Account {
  login: String
  subject: Subject
}

Сделать тоже самое для вводимых типов нельзя. Для этого есть ряд предпосылок, но отчасти это связано и с тем, что в качестве формата данных при транспорте используется json. Тем не менее, при выводе, для конкретизации типа используется поле __typename. Почему нельзя было сделать тоже самое при вводе — не очень понятно. Мне кажется, что эту проблему можно было бы решить немного изящнее, отказавшись от json при транспорте и введя свой формат. Что-то в духе:


union Subject = OrganiationInput | PersonInput

input AccountInput {
  login: String!
  password: String!
  subject: Subject!
}

# Создание акаунта для организации

{
  account: AccountInput {
    login: "Acme",
    password: "***",
    subject: OrganiationInput {
        title: "Acme Inc"
    }
  }
}

# Создание акаунта для частного лица
{
  account: AccountInput {
    login: "Acme",
    password: "***",
    subject: PersonInput {
        firstName: "Vasya",
        lastName: "Pupkin",
    }
  }
}

Но это породило бы необходимость написания дополнительных парсеров под это дело.


4. Дженерики


А что не так в GraphQL c дженериками? А всё просто — их нет. Возьмем до банального обычный для CRUD индексный запрос с пагинацией или курсором — не важно. Я приведу пример с пагинацией.


input Pagination {
  page: UInt,
  perPage: UInt,
}

type Query {
  users(pagination: Pagination): PageOfUsers!
}

type PageOfUsers {
  total: UInt
  items: [User!]!
}

а теперь для огранизаций


type Query {
  organizations(pagination: Pagination): PageOfOrganizations!
}

type PageOfOrganizations {
  total: UInt
  items: [Organization!]!
}

и так далее… как бы я хотел иметь для этого дела дженерики


type PageOf<T> {
  total: UInt
  items: [T!]!
}

тогда бы я просто писал


type Query {
  users(page: UInt, perPage: UInt): PageOf<User>!
}

Да тонны применений! Мне ли вам рассказывать о дженериках?


5. Неймспейсы


Их тоже нет. Когда количество типов в системе перваливает за полторы сотни, вероятность коллизий имен стремится к ста процентам.


И появляются всякие Service_GuideNDriving_Standard_Model_Input. Я уж не говорю о полноценных неймспейсах на разных эндпоинтах, как в SOAP (да-да — он ужасен, но неймспейсы там сделаны прекрасно). А хотябы несколько схем на одном эндпоинте с возможностью "шарить" типы между схемами.


Итого


GraphQL — хороший инструмент. Он прекрасно ложится на толерантную, микросервисную архитектуру, которая ориентирована, в первую очредь, на вывод информации, и несложный, детерминированный ввод.


Если же у вас имеются полиморфные сущности на ввод — у вас могут возникнуть проблемы.
Разделение типов ввода и вывода, а также отсутствие дженериков — порождают кучу писанины на пустом месте.


Graphql — это не совсем (а бывает и совсем не) про CRUD.


Но это не значит, что его нельзя есть :)


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

Комментарии (115)


  1. nod
    08.01.2019 19:49
    +2

    Прекрасная статья!

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

    С удовольствием жду продолжения!


    1. greabock Автор
      08.01.2019 20:04
      +1

      Спасибо! Очень рад, что «зашло» )


    1. PerlPower
      09.01.2019 01:53
      +2

      Вас не затруднит описать характер приложений, которые GraphQL, по вашему опыту, ускорил? Какая технология использовалась до GraphQL и в чем были ее недостатки? Сильно ли тяжелее стала обработка ошибок и исключений, особенно на клиенте? И какой стэк вы используете?


      1. greabock Автор
        09.01.2019 02:26

        Приложение писалось сразу на GraphQL. Характер приложения я уже дал в статье — не знаю что еще добавить, не нарушив NDA. Если совсем абстрактно — это BPM с полиморфной моделью услуг, субъектов. Всё это лежит в четырех измерениях (на самом деле — трёх, так как из пространственных, учитываются только гео-координаты ). Одной фразой — граф данных, действительно сложный.

        Выбор, собственно стоял между GraphQL, JSON-API и Protobuf. По внутренним соображениям — победил GraphQL.

        Серверную разработку (особенно прототипирование), в сравнении со слепленным на коленке REST, GraphQL не ускоряет от слова «совсем». Скорее даже тормозит. Но на долгой дистанции получается серьезный выигрыш. Вы пишите функционал один раз и получаете:
        1. Внутренний программный интерфейс для веб-интерфейса/сайта.
        2. Программный интерфейс для проприетарных мобильных/десктопных приложений.
        3. Публичный программный интерфейс для сторонних разработчиков.

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

        Что касается разработки на фронте — то у нас в большинстве случаев (если не были введены новые типы), фронт генерируется автотматически на основе интроспекции схемы. Новые же типы приходится досконально разбирать и описывать. Но только один раз — потом только баги править.
        Вообще, я не гуру фронтенда, и допустил кучу ошибок в дизайне архитектуры на фронте. Но в целом это работает.

        Кроме того, нам удалось изящно решить проблему ролей/прав используя graphql. Но это материал для отдельной статьи.

        Из технологий:

        на фронте — Vue + Apollo + самописный фрейм поверх vue
        на бэке — самописный монстрик на php слепленный из webonyx/graphql, railt/sdl, doctrine/orm, cboden/ratchet (для подписок)

        бд — postgresql,
        кеш — redis,
        поиск — elasticsearch


        1. ko11ega
          09.01.2019 06:04

          А вместо "самописного монстрика" Hasura не подходит вам, совсем? Боль от отсутствия дженериков, возможно стала бы не столь существенной?


          1. greabock Автор
            09.01.2019 17:49

            Когда мы только начинали про Hasurа никто ничего не слышал. Сейчас это выглядит очень интересно, но уже поздняк метаться (


        1. gnaeus
          09.01.2019 08:36
          +1

          А мне вот наоборот импонирует подход GraphQL именно в серверной реализации. Я имею в виду Query Resolvers и DataLoader. Когда у нас есть единая реляционная база, нам легко вытащить нужные данные. Но если данные приходят из разных источников — написание API-фасада это боль.


          А так мы можем объединить данные из реляционки, mongo, redis, да даже другого REST API. Лишь бы он поддерживал операцию getByIds<K, V>(ids: K[]): V[]. И все это без проблемы N + 1 запросов.


        1. niksamokhvalov
          09.01.2019 16:23
          +1

          Кроме того, нам удалось изящно решить проблему ролей/прав используя graphql. Но это материал для отдельной статьи.

          Это очень и очень интересная, больная тема. Даёшь статью!


          1. rraderio
            09.01.2019 16:40
            -1

            +1


        1. Sybe
          10.01.2019 02:40

          присоединяюсь к тем, кто ожидает статью про роли/права


  1. Peretyaka
    08.01.2019 20:14
    +1

    1. NON_NULL

    До этого не обращал внимание и все ОК было, а теперь тоже глаз режет, вот зачем так? :)

    Остальную боль подтверждаю, но плюсов пока больше.


  1. jreznot
    08.01.2019 20:48

    Жаль, что в статье вы не коснулись проблем безопасности. Правильно настроить безопасность приложения с учётом всех возможных вариантов запросов очень сложно и это одно и самых слабых мест GraphQL.


    1. rraderio
      08.01.2019 21:04

      Используйте Persisted Queries в продакшене
      twitter.com/leeb/status/829434814402945026


      1. VolCh
        09.01.2019 18:19

        Как быть с кастомизируемыми представлениями? Тащить всё или для каждой комбинации запрос создавать?


        1. rraderio
          09.01.2019 18:28

          Что вы имеете ввиду под кастомизируемыми представлениями?
          Кстати, как вы сделаете в не GraphQL?


    1. greabock Автор
      08.01.2019 21:07

      Оу, у меня этого есть. Вроде атак на идентификатор с «похищением» сущностей.
      Но вопросы безопасности, как правило, не касаются graphql на прямую. Graphql отвечает лишь за соблюдение структуры данных. Всё остальное ложится напрямую на плечи разработчика. Что-то можно валидировать через кастомные скаляры. Вы могли заметить в статье я использую тип `UInt` — как не сложно догадаться — это `unsigned integer`, это кастомный скаляр со своим валидатором, тоже самое может касаться и других полей. Часто вижу скаляры Email, Url, Phone. Но есть такие вещи, которые можно провалидировать только в рантайме -вроде владения/принадлежности.
      В общем, GraphQL не предоставляет каких-то специальны инструментов безопасности.


    1. jakobz
      10.01.2019 03:22

      У меня обратный опыт. С GraphQL одна сущность и ее поле читаются в одном месте в коде, в отличии от классического API. Соответственно доступ на чтение контроллировать проще. С обычным API каждый эндпоинт так и норовит выцепить какой-нибудь секьюрный кусочек из БД, и непонятно как с этим нормально бороться.


  1. rraderio
    08.01.2019 21:12

    Если поле отсутсвует, то что нужно сделать? Не обновлять его? Или уставновить его в null? Суть в том, что разрешить значение null и разрешить отсутсвие поля — это разные вещи. Тем не менее в GraphQL — это одно и тоже.

    github.com/facebook/graphql/issues/542

    Сделать тоже самое для вводимых типов нельзя. Для этого есть ряд предпосылок, но отчасти это связано и с тем, что в качестве формата данных при транспорте используется json. Тем не менее, при выводе, для конкретизации типа используется поле __typename. Почему нельзя было сделать тоже самое при вводе — не очень понятно.

    github.com/facebook/graphql/pull/395

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

    Было бы очень интерестно


  1. Veikedo
    08.01.2019 21:20
    +3

    Пункт 2. Разделение ввода и вывода это скорее благо (да здравствует cqs).


    На одном старом проекте одни и те же модельки (=классы) использовались и на вход и на выход. Вы не представляете как меня задолбало по коду смотреть какие поля и когда надо заполнять.


    Ваше предложение использовать атрибуты для указания где input/output сильно усложнило бы язык.
    Строгий и явный контракт лучше, как по мне.


    1. Vahman
      08.01.2019 22:28

      В большинстве приложений бесспорно. То что предлагает графкюэль иногда звучит странно. Хочешь всегда получать только то что тебе нужно? Сделай соответствующий апи, и подготовь подходящие запросы к бд.
      PS: прошу прощения, попал не в ту ветку с телефона


    1. greabock Автор
      09.01.2019 03:13

      Вы не представляете как меня задолбало по коду смотреть какие поля и когда надо заполнять.

      Для этого есть интроспекция схемы.


      Ваше предложение использовать атрибуты для указания где input/output сильно усложнило бы язык.

      Тут без субъективщины не обойтись. Мне это сильно упростило бы жизнь.


  1. maxzh83
    08.01.2019 21:24
    +3

    Меня в GraphQL пугают две вещи: производительность и безопасность. Вероятно, в вашем приложении вы не сталкивались ни с тем ни с другим, но вообще есть вопросы. Например, есть какое-то поле, которое очень затратно выбирать и по хорошему нужно использовать разные запросы на БД, например. Или нужно разграничить набор возвращаемых данных в зависимости от роли пользователя.


    1. Vahman
      08.01.2019 22:33

      разделяю опасения. В итоге все воткнется в запросы к БД, которые генерит очередная ORM, в силу «универсальности» не умеющая это делать достаточно хорошо


    1. rraderio
      08.01.2019 22:39

      В деве можно использовать GraphQL, а в проде GraphQL + Persisted Queries


    1. AlexunKo
      09.01.2019 01:21

      Согласен. Дополнительные абстракции со временем дают нехилые течи на нефункциональном уровне.


    1. greabock Автор
      09.01.2019 01:32

      Проблемы с производительностью небыли здесь затронуты потому, что это не проблемы GraphQL как языка. Это проблемы его реализаций. Что бы затронуть эти темы, мне пришлось бы писать материал под заголовком "что не так с apollo/webonyx/railt/graphcool". А это совсем другая история :)


      1. maxzh83
        09.01.2019 10:34
        +1

        это не проблемы GraphQL как языка. Это проблемы его реализаций

        Мне как прикладному разработчику это не так важно в языке проблема или в его реализации. Мне важно, что я или не могу решить эту проблему или для решения мне надо фактически отказаться от GraphQL. Про реализацию это действительно отдельная проблема, видел решения, когда выгребают из БД все данные, а потом отдают только то что запрашивают через graphQL.


        1. rraderio
          09.01.2019 16:56
          +1

          Если GraphQL API не публичный и предназначен для внутренних клиентов (мобильных или веб), можно использовать списки доступа и предварительно одобренные запросы(Persisted Queries). Клиенты требуют сервер выполнить такие запросы, указывая вместо запроса его идентификатор. Кажется, Facebook применяет такой подход.

          При девелопменте используем любые запросы, на бекенде делаем в лоб.
          На проде используем только предварительно одобренные запросы, которые уже можно оптимизировать так как запросы не могут быть любыми.


        1. springimport
          09.01.2019 18:40

          выгребают из БД все данные, а потом отдают только то что запрашивают через graphQL

          Вообще, не так это плохо как звучит, как по мне. Получается как rest, но меньше отдавать данных.


          1. maxzh83
            09.01.2019 21:52

            Получается как rest, но меньше отдавать данных.

            Ну и стоит ли городить еще одну абстракцию только ради экономии трафика? Ну и главное, в rest, если будет упор в производительность, можно сделать либо отдельный ендпоинт, либо добавить параметр и оптимизировать выдачу нужным образом. С GraphQL, конечно, можно сделать также, но зачем он тогда нужен?


            1. springimport
              10.01.2019 17:16

              Согласен что спорный момент. При этом трафик гораздо важнее чем кажется. У меня сейчас есть огромные структуры заказов и клиентов которые могут занимать даже мегабайты, а с графом занимали бы десятки килобайт.


              1. maxzh83
                10.01.2019 22:31

                Понятно, что от проекта зависит, но потенциально решается написанием дополнительных ендпоинтов с только нужными данными.


    1. Sybe
      09.01.2019 22:36

      Меня в GraphQL пугают две вещи: производительность и безопасность.
      Мне кажется, такие опасения возникают из-за восприятия GraphQL API как интерфейса к БД, хотя по факту GraphQL не привязан к источнику хранения данных.

      Представьте, что у вас есть REST-endpoint, который позволяет получить список каких-либо сущностей. В GraphQL вы можете использовать тот же самый код, который получает эти данные для REST. Только GraphQL позволяет отдавать пользователю только те поля, которые он запросил, таким образом, экономя трафик пользователя и уменьшая количество запросов, которые он должен сделать, чтобы получить необходимые данные.

      Например, данные вы можете получить с помощью локального HTTP-запроса к микросервису. Несколько запросов к микросервисам внутри локальной сети вашей компании для формирования GraphQL-ответа отработают намного быстрее, чем несколько запросов от пользователя к REST API.

      Например, есть какое-то поле, которое очень затратно выбирать и по хорошему нужно использовать разные запросы на БД
      Создайте отдельный resolver для этого поля и укажите, каким образом это поле должно извлекаться из хранилища.

      Или нужно разграничить набор возвращаемых данных в зависимости от роли пользователя
      А каким образом вы бы реализовали это в REST API? В GraphQL, например, разграничить доступ к полям можно с помощью директив, указанных на каждом поле, где необходима проверка доступа. Вот статья, описывающая такой подход: codeburst.io/use-custom-directives-to-protect-your-graphql-apis-a78cbbe17355, но это не единственный возможный вариант решения.


      1. Huan
        10.01.2019 13:14

        Я ничего против GraphQL не имею, но странную аргументацию о наборе полей слышу постоянно и у всех. Вероятно, вы работали с реализациями REST без этой возможности, но в большинстве фреймворков сегодня это есть. Мало того, что можно убрать какие-то поля, можно еще и детализацию вытащить.


      1. maxzh83
        10.01.2019 22:51

        Только GraphQL позволяет отдавать пользователю только те поля, которые он запросил, таким образом, экономя трафик пользователя и уменьшая количество запросов

        Это решается написанием нехитрого фасада, где в результате будет ровно один REST запрос под нужные данные, а заодно может еще кэширование с настройкой получения данных от внутренних запросов (circuit breaker) и т.д. И такой вариант позволит еще и уменьшить нагрузку на внутренние сервисы. Поскольку, если внутренний сервис отдает 20 полей, а нам надо 3 из них, то с GraphQL на выходе получим только уменьшение трафика, а нагрузка по получению этих 20 полей и прокачиванию их по внутренней сети останется.


        1. SerafimArts
          10.01.2019 23:03

          Это решается написанием нехитрого фасада, где в результате будет ровно один REST запрос под нужные данные,


          Только не один запрос, а 2^N разных запросов, включая критерии (аргументы) выборки для каждого поля, их отношения с другими сущностями и сопутствующие операции (ну, например, транзакционность нескольких параллельных запросов на 10 разных типов данных).

          У меня мозг расплавился от того, как только я начал представлять как это реализовать в RESTful реализации и сколько костылей придётся вставлять.

          Чтобы понимать — приведите пример того, как на RESTful выглядел бы запрос на получение пользователя по id и его 10 друзей в онлайне, где его createdAt обязан быть в формате RFC3339, а updatedAt, например, в виде отношения к дате создания.
          В GraphQL будет примерно так
          {
              user(id: 42) {
                  createdAt(format: RFC3339)
                  accountLife: updatedAt(diff: "createdAt")
                  friends(status: ONLINE, count: 10) {
                      ...
                  }
              }
          }
          


          1. maxzh83
            10.01.2019 23:12

            Только не один запрос, а 2^N разных запросов

            Нет, к фасаду будет один запрос. Внутри фасада будут запросы уже в зависимости от того, что требуется получить.
            Чтобы понимать — приведите пример того, как на RESTful выглядел бы запрос на получение пользователя по id и его 10 друзей в онлайне

            Не совсем понял, что вы имеете в виду без описания того, что где хранится. Но, ок, вот так будет выглядеть, как вариант:
            /user-with-online-friends/42?friendsCount=10

            Url можно придумать поизящнее, написал что пришло в голову. Если подобных запросов много, то разумно придумать какую-то систему или набор параметров.


            1. SerafimArts
              10.01.2019 23:25

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


              Я по-моему довольно чётко описал:
              1) Пользователь с id 42 (это вы сделали)
              2) Дата создания пользователя (у вас в примере этого нет)
              3) В формате RFC3339 (у вас в примере этого нет)
              4) Отношение даты создания к дате последней активности в произвольном формате (у вас в примере этого нет)
              5) Список друзей (у вас в примере этого нет)
              6) В количестве 10 штук (это есть)

              И прошу заметить — это совершенно тривиальный запрос для GraphQL API. т.е. я не выдумаю лишних сложностей.

              Сложности начинаются, когда ещё просят статистику активности этого пользователя с группировкой по X, Y, Z, в интервале A...B, с выборкой по какой-нибудь географии и отношению к другому пользователю. Я не то чтобы выдумаю, но похожие задачи у меня на работе были и когда это вертелось бы поверх REST — был лютейший трешак.


              1. maxzh83
                10.01.2019 23:42

                Все, что вы написали в пунктах 2,3,4,5 выдается на фасаде и содержится в результирующем json. Т.е. дата будет в нужном формате, есть друзья онлайн и т.д. Иначе говоря, под описанный вами случай пилится ендпоинт с разной степенью параметризованности. Для этого кейса пишется специальная dto и заполняется всем необходимым. Но, опять же, я это расписываю для одного случая. Если таких вариантов больше десятка-двух, то нужно отдельно продумывать как это лучше сделать. Но принципиальных проблем не вижу. Более того, такой подход позволяет нормально покрыть это все юнит-тестами.


                1. greabock Автор
                  11.01.2019 00:04

                  Все, что вы написали в пунктах 2,3,4,5… [очень много букв]… то нужно отдельно продумывать как это лучше сделать...

                  Или можно просто взять GraphQL )


                1. SerafimArts
                  11.01.2019 02:17

                  Если таких вариантов больше десятка-двух, то нужно отдельно продумывать как это лучше сделать.


                  Ну так я написал, что нужно учитывать все возможные ситуации и сочетания в количестве 2^N^M от полей и аргументов соответственно и попросил привести в пример частный запрос на RESTful эндпоинт, который удовлетворяет этому частному случаю, когда какому-то одному клиенту из сотен (или тысяч) потребовался конкретно этот кейс с перечисленными выше условиями.

                  Не думаю что выдумать такой адрес GET запрос и нужные параметры доставит вам проблем. Всего лишь приведите пример такого адреса и аргументов. Это так сложно?)))

                  Кажется не очень, т.к. на написание аналогичного запроса GraphQL мне потребовалось пара минут, а вы пытаетесь аргументированно доказать, что «гибкость запросов», которую приводят в пользу GraphQL — не аргумент, и можно тоже самое на RESTful реализовать без проблем.


                  1. maxzh83
                    11.01.2019 10:38

                    Не думаю что выдумать такой адрес GET запрос и нужные параметры доставит вам проблем.

                    Как же вам еще объяснить, что я не буду закладывать параметры для формата данных и т.д? Это должно быть заложено в реализацию. Написать как данные получить из БД и замапить в dto с преобразованием данных в нужный формат, вы этого хотите?
                    Еще раз, все зависит от контекста. Как правило вариантов хотелок и разных выборок ограниченное количество. И для каждого такого варианта пишется отдельный эндпоинт с осмысленным и коротким названием. Все. Если таких хотелок огромное множество, например как у Facebook, нужен какой-то механизм формирования запросов и тут GraphQL вероятно зайдет хорошо. Но, опять же, таких проектов по моему опыту абсолютное меньшинство, а благодаря хайпу, GraphQL пытаются прикрутить не потому, что он необходим, а потому что могут.


                    1. SerafimArts
                      11.01.2019 14:11

                      Это должно быть заложено в реализацию. Написать как данные получить из БД и замапить в dto с преобразованием данных в нужный формат, вы этого хотите?


                      Не думаю что выдумать такой адрес GET запрос и нужные параметры доставит вам проблем.


                      1. maxzh83
                        11.01.2019 14:19

                        /user-with-online-friends/42?friendsCount=10


              1. michael_vostrikov
                11.01.2019 11:20

                Можно что-нибудь такое придумать. В результате все сводится к написанию DTO с нужными геттерами.


                Скрытый текст
                // /user/42?fields=id,name,createdAt,accountLife,onlineFriends&limit[onlineFriends]=10
                
                class UserDTO
                {
                    protected $model;
                
                    public function __construct(UserModel $model)
                    {
                        $this->model = $model;
                    }
                
                    public function getCreatedAt()
                    {
                        return formatDate('RFC3339', $this->user->created_at)
                    }
                
                    public function getAccountLife()
                    {
                        return formatTimeDiff('RFC3339',
                            $this->user->updated_at, $this->user->created_at
                        );
                    }
                
                    /** @return ActiveQuery */
                    public function getOnlineFriends()
                    {
                        return $this->model->getFriends()
                            ->andWhere(['status' => 'online']);
                    }
                
                    public function __get($name)
                    {
                        $getter = 'get' . ucfirst($name);
                        $value = (method_exists($this, $getter)
                            ? $this->$getter()
                            : $this->user->__get($name));
                
                        return $value;
                    }
                }
                
                class UserModel
                {
                    /** @return ActiveQuery */
                    public function getFriends()
                    {
                        return $this->hasMany(User::class, ['second_user_id' => 'id'])
                            ->viaTable('friends', ['id' => 'first_user_id']);
                    }
                }
                
                class Engine
                {
                    ...
                
                    private function prepareFields()
                    {
                        ...
                
                        foreach ($fields as $field) {
                            $value = $modelDTO->__get($field);
                
                            if ($value instanceof ActiveQuery) {
                                $query = $value;
                                if (isset($this->request->limit[$field])) {
                                    $query->limit($this->request->limit[$field]);
                                }
                                $value = $query->all();
                            }
                
                            $result[$field] = $value;
                        }
                    }
                }


  1. Vahman
    08.01.2019 22:26
    +1

    То, что это инструмент, созданный для решения конкретных задач конкретной компанией. Сама идея просто крутая, но когда его пытаются прикрутить везде, где это возможно, или даже невозможно, становится намного странно.


  1. arvitaly
    09.01.2019 06:13

    Вот выше про производительность вы пишете, что это не проблемы GraphQL. А я вам больше скажу, все, что вы описали к языку никакого отношения не имеет. Потому что никто не мешает использовать абсолютно любой инструмент для генерации/чтения схемы.
    GraphqQL — это язык обмена данными, а не язык программирования общего назначения.

    К примеру, я абсолютно не понимаю, для чего использовать один и тот же тип для описания добавления сущности и обновления. Это разные операции, а желание возникает из-за использования подобного в REST/ORM моделях, лично я с этим не согласен. Это и называется мутации, а не обновление сущности и они могут быть сложными, вложенными и т.д. Обычный CRUD и так автоматизируется легко и непринужденно (в том числе, в GraphQL). Если хотите так все заабстрагировать, делайте это на стороне языка программирования.


    1. greabock Автор
      09.01.2019 09:51

      GraphqQL — это язык обмена данными, а не язык программирования общего
      назначения.

      GraphQL — это два языка: язык запросов (QL) и язык определения схемы (SDL). В материале я сделал упор на SDL, так как я активно его использую. Вы можете написать свою имплементацию для декларации схемы/интроспекции. В любом случае какую бы вы не написали реализацию — вам всё равно придется мапить это имеющуюся систему типов.


      К примеру, я абсолютно не понимаю, для чего использовать один и тот же тип для описания добавления сущности и обновления.

      А это и не должен быть один тип. Проблема описанная в этом примере вообще никак не касается разделения таких типов. И с разными типами будет точно такая же проблема.
      Читайте материал.


      Это и называется мутации, а не обновление сущности и они могут быть сложными, вложенными и т.д

      Я ожидал именно таких комментариев. И специально для этого сделал в начале материала ремарку, а конце материала написал: Graphql — это не совсем (а бывает и совсем не) про CRUD.
      Но видимо это так не работает. Читайте материал.


      Обычный CRUD и так автоматизируется легко и непринужденно (в том числе, в GraphQL).

      Нет. Читайте материал.


      Если хотите так все заабстрагировать, делайте это на стороне языка программирования.

      Про генерацию кода я тоже сказал. Читайте материал.


      Мне сейчас прям даже обидно. Я же писал для кого?


      1. arvitaly
        09.01.2019 10:08
        -2

        Давайте я упрощу для вас, статья называется «Что не так с GraphQL»? Следовательно, ее цель указать на какие-то недостатки GraphQL? Я не увидел в статье проблем, связанных с языком GraphQL, используемые инструменты вы не обсуждаете, о чем вообще речь? Если же это кликбейт-заголовок, а вы просто хотели рассказать о своем опыте использования языка GraphQL, то так и пишите.


        1. greabock Автор
          09.01.2019 10:22
          +2

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

          1. Null
          1.1 Вывернутые наизнанку монады.
          1.2 Неоднозначность nullable при вводе.
          2 Неудобное разделение ввода и вывода.
          3 Отсутствие полиморфизма при вводе.
          4 Отсутствие дженериков.
          5 Отсутствие неймспейсов.

          Прочитайте наконец материал, чтобы не приходилось в комментариях спрашивать автора о чем материал. Ну это уже просто неуважение какое-то (


          1. arvitaly
            09.01.2019 10:39
            +1

            Про null и восклицательные знаки — абсолютная вкусовщина, и я уже сказал, что это скорее вопрос к генерации/чтению схемы. Нет никаких проблем сделать генератор схемы, где поля будут обязательными (при этом в схеме они будут non_null).
            Нет никакой неоднозначности при вводе, просто не используйте PostInput и в create и в update, вы на это не ответили ничего (уважайте комментаторов, пожалуйста).
            Расскажите, в каком языке описания данных есть полиморфизм и дженерики, неймспейсы? А что дальше, лямбда-функции, циклы, условия? Зачем вообще тогда выдумывать и использовать декларативные языки и форматы, если можно просто взять любой готовый язык общего назначения и написать для него, к примеру, еще один компилятор?
            Почему все это становится недостатками GraphQL — абсолютно непонятно. Замените GraphQL на любое другое слово — смысл статьи не изменится. «Что не так с JSON»? Там нет дженериков. Что не так с HTML? Там нет полиморфизма.


            1. greabock Автор
              09.01.2019 11:07

              Про null и восклицательные знаки — абсолютная вкусовщина

              Я так и сказал.


              Нет никакой неоднозначности при вводе, просто не используйте PostInput и в create и в update

              input PostCreateInput  {
                text: String
              }
              input PostUpdateInput  {
                text: String
              }

              Куда делась неоднозначность? Всё верно — никуда.


              Расскажите, в каком языке описания данных есть полиморфизм и дженерики, неймспейсы.

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


              А что дальше, лямбда-функции, циклы, условия?

              А вот это уже рантайм. Но вообще-то без привязки лямбд, к полям типов у вас и приложение работать не будет...


              Зачем вообще тогда выдумывать и использовать декларативные языки и форматы, если можно просто взять любой готовый язык общего назначения и написать для него, к примеру, еще один компилятор?

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


              Почему все это становится недостатками GraphQL — абсолютно непонятно.

              Я уж как мог так объяснил. Не знаю как сделать, чтобы вам было понятно.


              Что не так с JSON

              Я в этой статье написал, что не так с JSON. Читайте уже материал )


              Что не так с HTML? Там нет полиморфизма.

              В HTML c полиморфизмом вообще-то всё ок )


              1. arvitaly
                09.01.2019 11:19
                -1

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

                Так я и не понимаю, почему вы хотите этого от GraphQL? Возьмите JS, если вам хочется свободы, многие базы данных так и сделали.
                Стандарты и протоколы хороши не тем, что в них напихано все на свете, а тем, что используя их можно сделать, что угодно дешево. Вот GraphQL позиционируется, как низкоуровневый формат, а сверху вы можете реализовать что угодно — дженерики, обязательные поля и т.д.

                В HTML c полиморфизмом вообще-то всё ок )

                Ну да, он вообще тьюринг-полный, кстати, почему бы вам не использовать его?


              1. arvitaly
                09.01.2019 11:25

                PostUpdateInput

                А тут нет никакой неоднозначности, если у вас 2 разных способа обновить сущность, то нужно делать 2 разных Input. Еще раз повторю — это мутации, а не обновления сущности Post, не нужно к ним так относиться! Это описание действия в бизнес-логике, а не универсальный CRUD-метод!


                1. greabock Автор
                  09.01.2019 18:04

                  Я просто процитирую сам себя:


                  Думаю, что стоит сделать небольшую ремарку относительно того, где я применяю данный язык. Это довольно сложная SPA-админка, большая часть операций в которой — это довольно нетривиальный CRUD(сложновложенные сущности). Значительная часть аргументации в данном материале связана именно с характером приложения и характером обрабатываемых данных. В приложениях другого типа (или с другим характером данных) таких проблем может и не возникнуть в принципе.

                  Вы не можете выкинуть CRUD из статьи о CRUD.


                  Умоляю: сжальтесь надо мной и прочитайте материал.


                  1. arvitaly
                    11.01.2019 10:21
                    -2

                    Вы можете сколько угодно переходить на личности, сути это не меняет.

                    Вы не можете выкинуть CRUD из статьи о CRUD.

                    Пожалуйста, сжальтесь над читателями и прочитайте свой заголовок, а затем список выдуманных претензий. Где в заголовке слово CRUD? Какое отношение дженерики, неймспейсы, null имеют непосредственно к CRUD, а не к чему угодно в программировании?
                    Вся статья заключается в том, что вы взяли инструмент, который вам не нужен, потом заявили, что в нем нет того, что нужно (неожиданно), а затем создали кликбейт-заголовок «Что не так в инструменте X» и не написали ни слова, что не так в инструменте X.


  1. greabock Автор
    09.01.2019 09:41
    +1

    Хотел ответить кому-то, и случайно нажал «отклонить» на комментарии.
    Простите, пожалуйста.
    Человек спрашивал, о планах по статье об организации ролей/прав.
    Очень хочу написать, если руки дойдут.


  1. vladar
    09.01.2019 11:27

    По факту, GraphQL — не всем подойдет, и может оказаться совсем не тем инструментом, который вам нужен.


    Дайте технологию про которую нельзя такое сказать. И даже на вашем примере эта мысль не вполне раскрыта. В вашем случае (сложные CRUD'ы) и т.п. — какой же инструмент подошел бы вам лучше в итоге?


    1. greabock Автор
      09.01.2019 19:21

      Конкретно для CRUD'a гораздо лучше зашел бы REST слепленный на коленке, без типизации и вот этих всех наворотов.


  1. domix32
    09.01.2019 12:27

    Так это ж отличный повод организовать новый формат обмена с генериками и немспейсами, как логичное продолжение. Ожидаю развитие вашей мысли (RFC?). Спасибо.


    1. greabock Автор
      09.01.2019 12:37

      Дженерики и неймспейсы ожидаются в ближайшее время в диалекте RL/SDL.
      Только вот маппить это всё, всё равно придется на имеющуюся систему типов.


  1. yleo
    09.01.2019 13:09

    Суть в том, что разрешить значение null и разрешить отсутствие поля — это разные вещи.

    Это действительно разные вещи, но вы их совершенно неверно трактуете:


    • Отсутствие поля = оно не определено в структуре/классе.
    • Если же поле определено, то оно "всегда есть", и в случае nullable может иметь значение NULL.

    Отступление от этого принципа ведет в тупик (требует разных NULL'ей, приводит к семантической путанице/неоднозначности). Это регулярно демонстрирует JSON (и другие "текстовые" языки описания данных).


    В этом ключе, пример с update не корректен, точнее говоря налицо проблема в дизайне API.


    1. andreymal
      09.01.2019 13:50
      +1

      Предложите тогда свой вариант, как обновить только одно поле объекта, не трогая остальные его поля. Я не пользую GraphQL (ещё), но тоже всегда просто опускал поля, которые не нужно обновлять, в этом самом JSON без какой-либо путаницы


      1. yleo
        10.01.2019 16:21

        JS (и JSON как следствие), на мой взгляд, являются языками с массой недочетов/нелогичностей ради "упрощения и удобства", которые ведут к массе проблем. В свою очередь GraphQL идет следом по тем же граблям, с добавлением пары новых.


        Если совсем кратко (и вынужденно высокопарно), то основная претензия в том, что предлагается замещение содержания формой представления информации, т.е. форма становится определяющей по отношению к содержанию.


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


        • доктор, я порезался.
        • не ешьте с ножа.
        • доктор, как правильно есть с ножа.
        • не ешьте с ножа.
        • доктор, как мне лучше есть с этого ножа.
        • не ешьте с ножа.


        1. andreymal
          10.01.2019 16:25
          +1

          — Доктор, как мне из обеда скушать только первое, а второе и компот оставить в покое?
          — Ешьте только весь обед целиком.

          На мой вопрос-то так и не ответили.


          1. yleo
            10.01.2019 16:50

            На мой вопрос-то так и не ответили.

            Ответ более-менее очевиден.


            Семантически нам нужно указать какие поля мы хотим обновить, а какие не трогать. В случае безсхемного JSON достаточно логично просто поместить в update-запрос только обновляемые поля с новыми значениями. Но возникают проблемы, если язык описания подразумевает наличия схемы (описание структур), из которого следует умолчание о null-значения у "отсутствующих" полей.


            Выход достаточно стандартный = передавать в аргументах update-запроса не экземпляр структуры, а массив структур key-value. [ { name: "field1", vallue: null }, {name: "field2", vallue: "not a null"}, ...]. Конечно, так теряется контроль схемы на уровне языка, но это следствие недостатков языка (формы описания). Тем не менее, так недостатки формы (языка) не перерастают в проблемы API.


    1. greabock Автор
      09.01.2019 14:19

      В таком случае, под проблему в дизайне API можно вообще подвести любой тезис из статьи )


      Немного оффтоп, но мне это напоминает, ситуацию, которая сложилась у меня с поддрежкой Razer.


      Была у меня клава с макросами. И решил я запилить макрос, который раз в час жмякает пару кнопок. Есть там такая модель макроса: нажал один раз — включил макрос, нажал второй — выключил. Но вот жеж косяк — если ты запустил макрос, то хотябы один раз он должен отработать. Нельзя отключить макрос до завершения текущей итерации и даже профиль переключить нельзя, пока макрос не завершен. И я, как программист, понимаю в чем проблема. Но мне, как пользователю, нужен функционал. Я написал в поддержку (ну, думаю, может в следующем патче на по поправят косяк). Описал суть проблемы с такими макросами, они очень долго не могли понять, что именно мне не нравится (тут, возможно, виноват мой не очень высокий скилл в заморском наречии). Потом они наконец сообразили в чем косяк, и сказали мне: "Не пишите такие макросы". А потом закрыли тикет. Заплатки на это дело нет по сей день.

      Так вот к чему это я… всегда можно сказать "не делай так". Вот только проблема от этого не исчезнет )


      1. yleo
        10.01.2019 16:27

        Проблема дизайна API тут в том, что семантика (смысл) запроса выражается с привязкой/зависимостью к неоднозначностям/недостаткам формы (языка описания запроса).


        Это не отменяет недостатков JSON и/или GraphQL, т.е. действительно есть недостатки, в том числе в выразительности и удобства для тех или иных случаев. Но вдвойне неверно допускать протечки этих недостатков в API.


        1. greabock Автор
          10.01.2019 16:43
          +1

          Я бы не смог сказать лучше. Собственно, в следующем материале я и хочу рассказать, как я избегаю подобных проблем.


  1. Kroid
    09.01.2019 17:08

    А я вот не понял, в чем проблема с null'ами?

    В мутациях отправляешь те поля, которые нужно изменить. Если не нужно изменять значение поля — не отправляешь его. Если отправляешь в качестве значения null, то и в бд запишется null, если валидация позволяет. Вы хотите, чтобы можно было отправлять поля со значениями null, а они при этом не меняли своего значения?

    В реляционных базах null — это отдельный вид значения, это не 0 и не пустая строка. Вот и GraphQL это отражает. Поэтому для какой-нибудь схемы {id: String!} соответствует значение {id: ""}, и не соответствует {id: null}.


    1. greabock Автор
      09.01.2019 18:31

      Допустим, как вы и сказали, я хочу отправлять только часть полей. При этом, если они переданы, то они не должны быть null. Как описать эту ситуацию в системе типов GraphqQL?


      1. Kroid
        09.01.2019 23:56

        А, теперь дошло. Действительно, в одну кучу смешали. По хорошему, сюда бы валидацию сделать толковую, как в grape, к примеру: обязательное/опциональное, список значений, возможность null-значения, min-max, регулярки, и так далее. Можно было бы парсить схему и на фронте не только генерировать, но и автоматически валидировать формы.


  1. rraderio
    09.01.2019 18:40

    А вы схему гененируете или пишите?


    1. greabock Автор
      09.01.2019 19:01

      Сама схема (почти полностью) пишется руками, из нее генерируется рефлексия схемы (railt/sdl), потом рефлексия проворачивается через мясорубку редюсеров, которые накидывают свои навороты (в том числе, там обрабатываются пользовательские директивы серверной стороны), и в итоге всё это маппится на схему типов webonyx. Получившаяся схема-webonyx закидывается в исполнитель-webonyx, который уже обрабатывает пользовательские запросы согласно схеме.


      1. rraderio
        09.01.2019 19:08

        Может быть если схема генерировалась из типов в приложении, то этих недостакков небыло бы?


        1. SerafimArts
          09.01.2019 19:32

          АПИ — это вью, типы в системе — это модели. В нормальном мире они не должны быть связаны, т.к. изменение внутренней логики не должно аффектить представление.


          1. rraderio
            09.01.2019 22:47

            мм, есть DTO для API, и есть Entity/модели, они не связаны. И из DTO можно генерировать GraphQL типы.


            1. SerafimArts
              10.01.2019 04:41

              Согласен. Только чем лучше это DTO нативного GraphQL SDL выражения? Те же утки, только в профиль.


              1. rraderio
                10.01.2019 11:24
                +1

                Эти DTO описаны ЯП приложения, т.е. там есть генерики, а по эти можно генерировать GQL схему.


                1. greabock Автор
                  10.01.2019 11:58

                  т.е. там есть генерики

                  это не точно )


                  Вообще, эта нормальная идея. Никто ж не против.


  1. RinNas
    09.01.2019 18:49

    Почему не подошёл OpenAPI?


    1. rraderio
      09.01.2019 18:53

      А как в OpenAPI клиенту точно указать какие данные ему нужны?


      1. RidgeA
        09.01.2019 23:25

        Примерно также как это делается в GraphQL — перечислением полей, которые надо получить.


        1. rraderio
          10.01.2019 11:43

          Можно пример или где это описано для OpenAPI?


    1. greabock Автор
      09.01.2019 19:12

      Главным образом, потому что хотелось единожды написать API под всех возможных потребителей. В случае с OpenAPI/JSON-API и проч. пришлось бы заниматься поддержкой нескольких разных схем для разных потребителей.


  1. summerwind
    10.01.2019 13:26

    1.2. INPUT

    А вот при вводе, nullable — это вообще отдельная история. Это косяк уровня checkbox в HTML

    Во-первых, перестаньте мыслить понятиями REST, когда работаете с GraphQL. Это другой подход, и мутации в GraphQL это не PATCH-методы из REST, а, скорее, аналог обычных функций в языках программирования. Когда, например, вы в любой ORM в метод «save» передаете объект как аргумент, вы же не можете каким-то образом «не передать» часть атрибутов этого объекта — вы либо передаете значения полей, которые изначально получили из БД, либо ставите null, и в БД тоже значение после сохранения становится null.
    Во-вторых, если у вас есть кейсы, где для одной и той же модели нужно обновлять разные группы полей, никто не мешает создать для этого разные мутации. Маловероятно, что у вас 50 различных комбинаций обновляемых полей — чаще всего это 2-3 кейса на модель. Даже если смотреть с точки зрения обычных языков программирования — лучше создать несколько методов, каждый под свои нужды, чем один супер-метод, который делает все на свете.

    4. Дженерики

    А что не так в GraphQL c дженериками? А всё просто — их нет.

    Зачем это нужно? Никто не пишет схему руками — для большинства языков программирования есть DSL для GraphQL с дженериками на уровне этого языка и всеми сопутствующими плюшками.


    1. rraderio
      10.01.2019 13:30

      Когда, например, вы в любой ORM в метод «save» передаете объект как аргумент, вы же не можете каким-то образом «не передать» часть атрибутов этого объекта

      Могу, если перегрузить функцию.


      1. mayorovp
        10.01.2019 13:55

        … и это будет уже другая функция.


        1. rraderio
          10.01.2019 13:57

          Да, но то же самое имя, не надо каждый раз изобретать новое имя.


    1. greabock Автор
      10.01.2019 14:31

      вы либо передаете значения полей, которые изначально получили из БД, либо ставите null

      Именно так. Только не "либо ставите" null, а "либо устанавливается дефолтное значение".
      Например вот так:


      input ExampleInput {
        value: Int = 0
      }

      И, что бы это поведение работало парвильно, нужно расценивать вот это выражение:


      input ExampleInput {
        value: Int
      }

      как


      input ExampleInput {
        value: Int = null
      }

      Но тогда косяк в том, что концепция partial input просто перестает работать.


      Другими словами, я не считаю понятия undefined и null тождественными.


      Если кому-то и так норм — я рад за них, мне — не норм.


      1. summerwind
        10.01.2019 16:00

        Именно так. Только не «либо ставите» null, а «либо устанавливается дефолтное значение».

        Да, естественно, но это не меняет сути. Я думаю, вас также не устроило бы, если бы все атрибуты, которые вы не передали, сбрасывались бы на дефолтные значения.

        Но тогда косяк в том, что концепция partial input просто перестает работать.

        Другими словами, я не считаю понятия undefined и null тождественными.

        Это не косяк, это ваша привычка мыслить подходами REST. Рассматривайте мутации GraphQL как просто функции в любом языке программирования, а input-типы как объекты в ООП (хорошо, без возможности иметь дефолтные значения атрибутов), и все станет восприниматься гораздо понятнее.


        1. rraderio
          10.01.2019 18:44

          Рассматривайте мутации GraphQL как просто функции в любом языке программирования, а input-типы как объекты в ООП

          Ok, у нас есть обьект с 10 полями, как быть если мы хотим обновить только 5 из нихЁ остальным не меняя значение. Создавать по фуникции для каждой комбинации?


          1. greabock Автор
            10.01.2019 18:58

            У меня есть решение, но придется обождать пока допишу материал )


          1. summerwind
            10.01.2019 19:31

            Предлагаю вместо абстрактных рассуждений рассмотреть конкретный пример.
            Скажем, есть такой тип:

            type User {
              login: String!
              role: String!
              name: String
              email: String
              age: Int 
            }
            


            Предположим, что в нашем приложении есть две формы: 1. Форма обновления «credentials» 2. Форма обновления «профиля». В первой форме нужно обновлять только поля «login», «role», а во второй поля «name», «email», «age».
            Есть два варианта решения этой задачи:
            Вариант 1, не очень удачный на мой взгляд — делаем input тип со всеми полями и одну мутацию:

            input UserInput {
              login: String!
              role: String!
              name: String
              email: String
              age: Int
            }
            
            type Mutation {
              updateUser(input: UserInput!): User
            }
            


            Перед отображением форм достаем юзера с сервера через api. В первой форме поля «login» и «role» подставляем в UserInput из формы, остальные поля из того, что пришло из api. Во второй форме поля «name», «email», «age» подставляем в UserInput из формы, остальные поля из того, что пришло из api. Вызываем updateUser с заполненным UserInput.

            Вариант 2, более адекватный — делаем свой input тип для каждого кейса и две мутации:

            input UserCredentialsInput {
              login: String!
              role: String!
            }
            
            input UserProfileInput {
              name: String
              email: String
              age: Int
            }
            
            type Mutation {
              updateUserCredentials(input: UserCredentialsInput!): User
              updateUserProfile(input: UserProfileInput!): User
            }
            


            В каждой форме используем соответствующий input и мутацию.


            1. greabock Автор
              10.01.2019 19:46

              Это не подходит для CRUD. Потому, что это хреново ложится на генераторы, или генераторы становятся слишком сложными. Это делается лучше и проще.


              1. summerwind
                10.01.2019 19:53

                Можно какие-то более конкретные примеры? Возможно, я просто не до конца понимаю, в чем такая огромная сложность в цепочке «взять объект, полученный из api» > «заменить поля, которые пользователь отредактировал» > «вызвать мутацию».
                Может, это и делается немного проще с точки зрения отправки на сервер из фронтенда, но путем создания монструозных типов на сервере, где какой-нибудь Boolean может иметь не 2 значения (true/false), а 3 (true/false/undefined), что на мой взгляд еще хуже.


                1. greabock Автор
                  10.01.2019 20:03

                  Вызвать какую мутацию? У вас их две. А с ростом количества полей, их количество также будет расти. Подождите немного, я подробно разберу эту тему, в одном из следующих материалов (я пока не решил какая из тем пойдет вперед — народ сильно топит за "разграничение доступа").


                  1. summerwind
                    10.01.2019 20:11

                    Вызвать какую мутацию? У вас их две.

                    Посмотрите первый вариант в моем комментарии выше. Я специально написал его для любителей выполнять одним методом все на свете. Второй вариант с двумя мутациями — это для приложений с четкими различающимися кейсами изменения модели. Например, отдельная форма изменения email, отдельная форма обновления имени и фамилии и т.п. Очевидно, что там даже на бэкенде логика разная будет выполняться в зависимости от формы, и делать это все одним методом с кучей if'ов, мягко говоря, не айс.

                    Также непонятно, если у вас CRUD, и на каждую сущность форма со всеми полями, зачем отправлять только часть из них, если у вас на руках полная модель.


                    1. greabock Автор
                      10.01.2019 20:21

                      Оу, простите, виноват. Я почему-то подумал, что это про второй вариант.


                      Также непонятно, если у вас CRUD, и на каждую сущность форма со всеми полями, зачем отправлять только часть из них, если у вас на руках полная модель.

                      Чтобы реализовать концепцию partial update — отправлять только то, что было изменено. Прошу вас, потерпите немного. Иначе мне придется выложить все свои соображения по этому поводу в комментариях, а потом написать пост единственным содержимым которого будут ссылки на эти комментарии )


                      1. summerwind
                        10.01.2019 20:41

                        Ок, будет интересно почитать) Только хотел бы попросить кое-что, если не трудно — напишите, пожалуйста, в будущей статье, в чем основной профит от предлагаемого подхода. Потому что я на 90% уверен, что в GraphQL partial update реализуется только усложнением входных типов. А делать partial update ради partial update, либо бороться против 2Кб дополнительного трафика (или использовать это как основное средство против race conditions) выглядит не очень хорошей мотивацией)


                        1. Kroid
                          10.01.2019 23:18
                          +1

                          Смысл partial update в том, что пока Вася обновляет описание сущности, Игорь добавляет туда фотографии или меняет цену, или название. Если обновлением перезаписывать все поля, то есть большой шанс откатывать таким образом чьи-то изменения.


                          1. summerwind
                            10.01.2019 23:52

                            Вы всерьез верите, что partial update — хорошее средство защиты от перезаписи при одновременном редактировании несколькими пользователями? :) Если Вася и Игорь одновременно загрузили страничку с формой обновления сущности, Вася поменял описание и цену, а Игорь поменял название и исправил опечатку в описании, то partial update никак не защитит от того, что Игорь перезапишет все изменения Васи в поле описания сущности. В таких случаях применяется что-то более нормальное — например, optimistic lock.


                            1. Kroid
                              11.01.2019 00:03

                              optimistic lock
                              И как вы его примените в этой ситуации, позвольте спросить.


                              1. summerwind
                                11.01.2019 00:33

                                Так, как это обычно и делается в самом классическом его применении — мы увидим, что в изменениях Игоря есть конфликты и покажем ему предупреждение \ выкинем исключение, etc. Уточните, какой именно момент непонятен.


                                1. Kroid
                                  11.01.2019 00:43

                                  Как мы узнаем, что есть конфликты? Вася открыл форму редактирования и ушёл к начальству уточнять вопрос. В это время Игорь отредактировал пару полей. Через 10 минут Вася приходит, редактирует поля в форме и сохраняет их — на сервер идёт запрос и обновляется сущность целиком — Вася не знал, что пара полей уже изменилась. А бэк подумал, что Вася изменил больше полей, чем это есть на самом деле. В итоге изменения Игоря откатились по факту.

                                  Как именно вы в таком сценарии оптимистичную блокировку внедрите? Сам принцип какой будет? Синхронизировать данные вебсокетами, как в гугл доксе? Открывая форму редактирования, отправлять серверу дополнительный запрос вида «сейчас я начну редачить вот эту сущность»?

                                  Upd А, кажется, понял. Отправлять не только новые значения, но и старые — чтобы сервер сравнивал, совпадают ли эти старые значения, перед обновлением. Мороки это, конечно, добавит знатно ;)


                                  1. summerwind
                                    11.01.2019 01:13

                                    Эх… Если Вася открыл форму и ушел гулять, а Игорь в это время что-то сохранил, то Вася потом ничего не затрет, а, наоборот, получит алерт. Почитайте, пожалуйста, про принципы, на которых реализуется обычно optimistic lock — версионность и т.п., как это сделано в разных Wiki, в issue tracking системах типа Redmine и других.


                                    1. Kroid
                                      11.01.2019 01:23
                                      +1

                                      Это мы на ровном месте усложняем наше приложение — как в разработке, так и в конечном интерфейсе. Васе и Пете не нужна версионность, им просто нужна возможность совместно управлять контентом, к примеру, интернет магазина. Partial update это обеспечивает в большинстве случаев, без дополнительных затрат.


                                      1. summerwind
                                        11.01.2019 01:49
                                        -1

                                        Приводите, пожалуйста, аргументы или какую-то статистику к своим выводам, а то обсуждение получается немного бесполезным.
                                        Optimistic lock это просто и поддерживается некоторыми ORM вообще из коробки. Надеяться на partial update как на основное решение при совместном редактировании считаю непрофессиональным. Где-то оно, может, и будет помогать худо-бедно, а где-то, где, скажем, поле «описание» у товара редактируется в 10 раз чаще других полей — не будет.


                                        1. Kroid
                                          11.01.2019 01:54

                                          Пожалуй, возьму паузу. Надо бы поподробней посмотреть на реализации. Из коробки я помню подобное только в эластик серче — автоматически создаётся новая версия при апдейте.


                      1. SerafimArts
                        10.01.2019 21:12

                        UPD. Тут был спойлер, но больше его нет. Простите, больше не буду


                        1. greabock Автор
                          10.01.2019 21:17

                          Да ну тебя, Кир… всю интригу развалил…
                          Хотел статью написать, а теперь придется в Доту играть (


                          1. SerafimArts
                            10.01.2019 21:26

                            Fixed


                          1. Kroid
                            10.01.2019 23:15

                            Воу-воу, а не видел, что там было написано. Так что пишите статью, мы все её ждём :)


  1. Zhanadil1509
    10.01.2019 14:19

    Обычно на любую проблему есть решение. Бывает костылят, бывает хорошие решения. Знатоки в этой области предоставили бы решения(е) на эти проблемы