Что не так с GraphQL

    В последнее время 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.


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


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

    Поделиться публикацией

    Похожие публикации

    Комментарии 132

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

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

      С удовольствием жду продолжения!
        +3
        Спасибо! Очень рад, что «зашло» )
          +3
          Вас не затруднит описать характер приложений, которые GraphQL, по вашему опыту, ускорил? Какая технология использовалась до GraphQL и в чем были ее недостатки? Сильно ли тяжелее стала обработка ошибок и исключений, особенно на клиенте? И какой стэк вы используете?
            +1
            Приложение писалось сразу на 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
              0

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

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

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


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

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

                  Это очень и очень интересная, больная тема. Даёшь статью!
                  0
                  присоединяюсь к тем, кто ожидает статью про роли/права
              +5
              1. NON_NULL

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

              Остальную боль подтверждаю, но плюсов пока больше.
                +6
                Жаль, что в статье вы не коснулись проблем безопасности. Правильно настроить безопасность приложения с учётом всех возможных вариантов запросов очень сложно и это одно и самых слабых мест GraphQL.
                  0
                  Используйте Persisted Queries в продакшене
                  twitter.com/leeb/status/829434814402945026
                    0
                    Как быть с кастомизируемыми представлениями? Тащить всё или для каждой комбинации запрос создавать?
                      0
                      Что вы имеете ввиду под кастомизируемыми представлениями?
                      Кстати, как вы сделаете в не GraphQL?
                    +1
                    Оу, у меня этого есть. Вроде атак на идентификатор с «похищением» сущностей.
                    Но вопросы безопасности, как правило, не касаются graphql на прямую. Graphql отвечает лишь за соблюдение структуры данных. Всё остальное ложится напрямую на плечи разработчика. Что-то можно валидировать через кастомные скаляры. Вы могли заметить в статье я использую тип `UInt` — как не сложно догадаться — это `unsigned integer`, это кастомный скаляр со своим валидатором, тоже самое может касаться и других полей. Часто вижу скаляры Email, Url, Phone. Но есть такие вещи, которые можно провалидировать только в рантайме -вроде владения/принадлежности.
                    В общем, GraphQL не предоставляет каких-то специальны инструментов безопасности.
                      +1
                      У меня обратный опыт. С GraphQL одна сущность и ее поле читаются в одном месте в коде, в отличии от классического API. Соответственно доступ на чтение контроллировать проще. С обычным API каждый эндпоинт так и норовит выцепить какой-нибудь секьюрный кусочек из БД, и непонятно как с этим нормально бороться.
                      +1
                      Если поле отсутсвует, то что нужно сделать? Не обновлять его? Или уставновить его в null? Суть в том, что разрешить значение null и разрешить отсутсвие поля — это разные вещи. Тем не менее в GraphQL — это одно и тоже.

                      github.com/facebook/graphql/issues/542

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

                      github.com/facebook/graphql/pull/395

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

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

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


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


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

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

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


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

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

                            +5
                            Меня в GraphQL пугают две вещи: производительность и безопасность. Вероятно, в вашем приложении вы не сталкивались ни с тем ни с другим, но вообще есть вопросы. Например, есть какое-то поле, которое очень затратно выбирать и по хорошему нужно использовать разные запросы на БД, например. Или нужно разграничить набор возвращаемых данных в зависимости от роли пользователя.
                              +1
                              разделяю опасения. В итоге все воткнется в запросы к БД, которые генерит очередная ORM, в силу «универсальности» не умеющая это делать достаточно хорошо
                                0
                                В деве можно использовать GraphQL, а в проде GraphQL + Persisted Queries
                                  0
                                  Согласен. Дополнительные абстракции со временем дают нехилые течи на нефункциональном уровне.
                                    +1

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

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

                                      Мне как прикладному разработчику это не так важно в языке проблема или в его реализации. Мне важно, что я или не могу решить эту проблему или для решения мне надо фактически отказаться от GraphQL. Про реализацию это действительно отдельная проблема, видел решения, когда выгребают из БД все данные, а потом отдают только то что запрашивают через graphQL.
                                        +1
                                        Если GraphQL API не публичный и предназначен для внутренних клиентов (мобильных или веб), можно использовать списки доступа и предварительно одобренные запросы(Persisted Queries). Клиенты требуют сервер выполнить такие запросы, указывая вместо запроса его идентификатор. Кажется, Facebook применяет такой подход.

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

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

                                            Ну и стоит ли городить еще одну абстракцию только ради экономии трафика? Ну и главное, в rest, если будет упор в производительность, можно сделать либо отдельный ендпоинт, либо добавить параметр и оптимизировать выдачу нужным образом. С GraphQL, конечно, можно сделать также, но зачем он тогда нужен?
                                              0
                                              Согласен что спорный момент. При этом трафик гораздо важнее чем кажется. У меня сейчас есть огромные структуры заказов и клиентов которые могут занимать даже мегабайты, а с графом занимали бы десятки килобайт.
                                                0
                                                Понятно, что от проекта зависит, но потенциально решается написанием дополнительных ендпоинтов с только нужными данными.
                                        +1
                                        Меня в 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, но это не единственный возможный вариант решения.
                                          0
                                          Я ничего против GraphQL не имею, но странную аргументацию о наборе полей слышу постоянно и у всех. Вероятно, вы работали с реализациями REST без этой возможности, но в большинстве фреймворков сегодня это есть. Мало того, что можно убрать какие-то поля, можно еще и детализацию вытащить.
                                            0
                                            Только GraphQL позволяет отдавать пользователю только те поля, которые он запросил, таким образом, экономя трафик пользователя и уменьшая количество запросов

                                            Это решается написанием нехитрого фасада, где в результате будет ровно один REST запрос под нужные данные, а заодно может еще кэширование с настройкой получения данных от внутренних запросов (circuit breaker) и т.д. И такой вариант позволит еще и уменьшить нагрузку на внутренние сервисы. Поскольку, если внутренний сервис отдает 20 полей, а нам надо 3 из них, то с GraphQL на выходе получим только уменьшение трафика, а нагрузка по получению этих 20 полей и прокачиванию их по внутренней сети останется.
                                              0
                                              Это решается написанием нехитрого фасада, где в результате будет ровно один 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
                                                Только не один запрос, а 2^N разных запросов

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

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

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


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

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

                                                  Сложности начинаются, когда ещё просят статистику активности этого пользователя с группировкой по X, Y, Z, в интервале A...B, с выборкой по какой-нибудь географии и отношению к другому пользователю. Я не то чтобы выдумаю, но похожие задачи у меня на работе были и когда это вертелось бы поверх REST — был лютейший трешак.
                                                    0
                                                    Все, что вы написали в пунктах 2,3,4,5 выдается на фасаде и содержится в результирующем json. Т.е. дата будет в нужном формате, есть друзья онлайн и т.д. Иначе говоря, под описанный вами случай пилится ендпоинт с разной степенью параметризованности. Для этого кейса пишется специальная dto и заполняется всем необходимым. Но, опять же, я это расписываю для одного случая. Если таких вариантов больше десятка-двух, то нужно отдельно продумывать как это лучше сделать. Но принципиальных проблем не вижу. Более того, такой подход позволяет нормально покрыть это все юнит-тестами.
                                                      +1
                                                      Все, что вы написали в пунктах 2,3,4,5… [очень много букв]… то нужно отдельно продумывать как это лучше сделать...

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

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


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

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

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

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


                                                            Не думаю что выдумать такой адрес GET запрос и нужные параметры доставит вам проблем.
                                                              0
                                                              /user-with-online-friends/42?friendsCount=10
                                                                0
                                                                Только не всего юзера, а только избранные поля. Потому что какой-то избранной реализации под мобилки не нужна остальная информация, а загружать канал как и сервера, так и клиента — накладно.

                                                                Я думаю, что в данном случае адрес, который отвечает заданным критериям должен выглядеть как-то так:
                                                                /user-with-createdAt-using-rfc3339-with-updatedAt-diff-with-createdAt-with-online-friends/42?friendsCount=10
                                                                -

                                                                Верно?

                                                                    +1
                                                                    Хорошо, это вполне себе годный костыль, согласен. Он прокатит для простых вещей. Раньше у нас так и было.

                                                                    А теперь представьте себе какую-нибудь серьёзную АПИшку, где данных довольно много:
                                                                    Вполне себе реальный продакшн (кликабельно)



                                                                    И закостылить такие штуки — это надо быть откровенным мазохистом.
                                                                      0
                                                                      Да как бы по факту ничего революционного в итоге нет.

                                                                      Простой апи — перечисление полей, которые нужно вернуть:
                                                                      /users?fields=id,name,..


                                                                      Сложный апи — используй силу json, Люк:
                                                                      POST /users/search.json
                                                                      {
                                                                       search: {...}
                                                                       output_format: {...}
                                                                      }
                                                                      

                                                                      , в {...} можно запихнуть конфигурацию любого уровня сложности

                                                                      Потом такие «а давайте одну точку входа сделаем!» — не вопрос:
                                                                      POST /graphql
                                                                      {
                                                                        users(...search_params...): {
                                                                          ...
                                                                        }
                                                                      }
                                                                      

                                                                      По сути тот же json, просто другой синтаксис — но фейсбук же не идет проторенными путями, верно?

                                                                      Жирный плюс — они написали спецификацию. Надо её теперь дополнить и стандартизировать.

                                                                      К примеру, одни фильтруют так:
                                                                      {
                                                                        users(id: 12) {
                                                                          id
                                                                        }
                                                                      }
                                                                      


                                                                      Другие — так:
                                                                      {
                                                                        users(id: {eq: 12}) {
                                                                          id
                                                                        }
                                                                      }
                                                                      


                                                                      Третьи так:
                                                                      {
                                                                        users(_eq: [id, 12]) {
                                                                          id
                                                                        }
                                                                      }
                                                                      


                                                                      Сделать единый формат, который будет поддерживать большинство из возможностей SQL WHERE, сделать инструменты на фронте и на сервере, которые это все генерируют и транслируют (типа hasura), и даже домохозяйка сможет создавать крутые SPA сайты. Мир, дружба, жвачка, и безработные айтишники, которые раньше делали «сайт под ключ за 5к рублей». А еще — генерация крутых админок под любой бекенд, был бы только в наличии дизайн приятный :)
                                                                        +1
                                                                        О, я и вёл к этому. Ждал хоть какого-то комментария после которого всем участникам станет ясно, что все велосипеды поверх RESTful приведут в конце концов к чему-то похожему на GraphQL =)

                                                                        Только эта мысль должна была придти тем, кто хотел доказать, что и на RESTful можно писать сложные штуки. Тогда бы я смог аргументированно высказаться, мол «а зачем это, когда всё и так уже есть», кинув ссылку на спеки GraphQL. Но это уже не суть.
                                                                    0
                                                                    Верно?

                                                                    Конечно нет. Что ж так тянет все усложнять. Я предлагаю еще упростить:
                                                                    Чтобы понимать — приведите пример того, как на RESTful выглядел бы запрос на получение пользователя по id и его 10 друзей в онлайне

                                                                    Вам это же для чего-то же было нужно? Вот и заведите ендпоинт под этот кейс: /yourcase?userId=… — все, там будут выдаваться только нужные поля, только 10 онлайн друзей и с нужным форматом даты. Такой способ позволяет полностью контролировать выдачу и по возможности избавиться от пустой нагрузки. В вашем изначальном требовании не было условия, что таких запросов может быть много с разным набором параметров. Но выше уже предложили решения и для этого случая.
                                                                      +1
                                                                      Да как вы не поймёте. Что не мне нужно, а всем тем сотням или тысячам клиентов, которые будут использовать этот API. У них совершенно разные задачи могут быть и совершенно разные реализации.

                                                                      Даже в отдельном проекте отдел фронтэнда — это тоже некие «они» со своим бизнес-планом у которых своё виденье того, как нужно, где и что загружать.

                                                                        +1
                                                                        Что не мне нужно, а всем тем сотням или тысячам клиентов, которые будут использовать этот API.

                                                                        Какие сотни тысяч клиентов? В статье написан пример использования — SPA-админка, у которой есть фронт, есть бэк. Все, нет никаких тысяч клиентов. Если у вас именно такой проект, то да, вам лучше использовать GraphQL, но не надо распространять это автоматом на все проекты в мире.
                                                                        это тоже некие «они» со своим бизнес-планом у которых своё виденье того, как нужно

                                                                        Для этого есть процесс интеграции фронта с бэком, бэк не делает сферического коня в вакууме, а так или иначе знает что нужно будет фронту. Взаимодействовать на этапе разработки все равно придется.
                                                                        На этом предлагаю завершать дискуссию, которая явно зашла в тупик.
                                                                          +3
                                                                          Я не разрабатываю админку, я разрабатываю приложение с программным интерфейсом, работающее в парадигме CRUD. Я стараюсь спроектировать API приложения таким образом, чтобы было максимально удобно написать под него генераторы интерфейса на основе интроспекции. Сегодня я понятия не имею, какие еще клиенты у этого приложения появятся в дальнейшем. Тысяч и миллионов там точно не будет, но различная вариативность будет присутствовать 100%. Уже на текущем этапе кроме админки у приложения есть еще два вида клиента (один — агрегатор данных, второй — веб-сайт (личный кабинет)). Еще планируется третий вид клиента — сдк для интеграции с нашим API. Я не написал ни строчки кода отдельно под эти клиенты, хотя наборы данных, которые используют разные виды клиентов — существенно отличаются друг от друга. Просто каждый из них использует то, что ему нужно — и я никак не вмешиваюсь в этот процесс, кроме разграничения прав.
                                                          0

                                                          Можно что-нибудь такое придумать. В результате все сводится к написанию 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
                                                            На самом деле, вашу основную мысль я понимаю. Действительно зачем писать велосипеды, раз уже есть GQL. Но ведь сам GQL такой же велосипед, просто от авторитетных чуваков. Много уже писали, что есть и другие хорошие спеки, типа JSON-RPC, которые для многих задач могут оказаться не то что хуже, но и лучше чем GQL.

                                                            Кроме того, более-менее рабочий вариант GQL можно сотворить и просто на более стандартных подходах и форматах, используя совершенно стандартные инструменты, коих полно на всех возможных ЯП. А это, в свою очередь, тоже профит.

                                                            Ниже вариант решения вашей задачи без использования специальной спеки типа GQL. Единственная вводная заключается в том, что сервер «обучен» понимать что фильтрация может идти по любому полю таблицы (поля без "_") и при этом имеются специальные «директивы», которые описывают дополнительные действия и начинаются со знака "_" (тоже довольно общепринятый подход). Сами понимаете что научить такому сервер очень просто (для упрощения все на JS):

                                                            // client
                                                            import qs from 'qs';
                                                            ...
                                                            const usedId = 42;
                                                            const query = qs.stringify({
                                                                createdAt: {
                                                                   _format: 'YYYY-MM-DDTHH:mm:ssZ'
                                                                },
                                                                _join: {
                                                                    friends: {
                                                            	    status: 'ONLINE',
                                                            	    _limit: 10
                                                            	},
                                                            	accountLife: {
                                                                        updatedAt: {
                                                                            _diff: 'users.createdAt'
                                                                        },
                                                            	    _include: ['updatedAt', 'sid']
                                                            	}
                                                                }
                                                            });
                                                            
                                                            fetch(`/v1/users/${usedId}?${query}`).then(console.log).catch(console.error);
                                                            


                                                            // server
                                                            import qs from 'qs';
                                                            ...
                                                            server.get('/users/:id', (req, res, next) => {
                                                                console.log(req.params.id); // id юзера
                                                                console.log(req.query); // полностью восстановленный объект запроса
                                                            });
                                                            


                                                            В итоге визуально немного похоже на GQL, но с одной большой разницей — тут не используются никакие специализированные решения. Обычные объекты, обычный url-encoded, обычный REST. Да и серверный код для обработки этого также будет довольно примитивен. При этом остается все плюсы REST подхода, а сами запросы очень просто ложаться на SQL.

                                                              0
                                                              Признаться, идея откровенно прикольная. Можно будет подумать на счёт неё.
                                                  +1

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

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

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

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


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

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


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

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


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

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


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

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


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

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

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

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

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


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

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

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


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

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


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

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


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

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


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

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


                                                              Что не так с JSON

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


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

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

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

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

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

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

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

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


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

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


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

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

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

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


                                                          Дайте технологию про которую нельзя такое сказать. И даже на вашем примере эта мысль не вполне раскрыта. В вашем случае (сложные CRUD'ы) и т.п. — какой же инструмент подошел бы вам лучше в итоге?
                                                            0
                                                            Конкретно для CRUD'a гораздо лучше зашел бы REST слепленный на коленке, без типизации и вот этих всех наворотов.
                                                            0
                                                            Так это ж отличный повод организовать новый формат обмена с генериками и немспейсами, как логичное продолжение. Ожидаю развитие вашей мысли (RFC?). Спасибо.
                                                              0
                                                              Дженерики и неймспейсы ожидаются в ближайшее время в диалекте RL/SDL.
                                                              Только вот маппить это всё равно придется на имеющуюся систему типов.
                                                              0
                                                              Суть в том, что разрешить значение null и разрешить отсутствие поля — это разные вещи.

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


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

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


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

                                                                +1

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

                                                                  0

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


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


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


                                                                  • доктор, я порезался.
                                                                  • не ешьте с ножа.
                                                                  • доктор, как правильно есть с ножа.
                                                                  • не ешьте с ножа.
                                                                  • доктор, как мне лучше есть с этого ножа.
                                                                  • не ешьте с ножа.
                                                                    +1
                                                                    — Доктор, как мне из обеда скушать только первое, а второе и компот оставить в покое?
                                                                    — Ешьте только весь обед целиком.

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

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


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


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

                                                                  0

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


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


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

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

                                                                    0

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


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

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

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

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

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

                                                                      +1
                                                                      А, теперь дошло. Действительно, в одну кучу смешали. По хорошему, сюда бы валидацию сделать толковую, как в grape, к примеру: обязательное/опциональное, список значений, возможность null-значения, min-max, регулярки, и так далее. Можно было бы парсить схему и на фронте не только генерировать, но и автоматически валидировать формы.
                                                                    0
                                                                    А вы схему гененируете или пишите?
                                                                      0
                                                                      Сама схема (почти полностью) пишется руками, из нее генерируется рефлексия схемы (railt/sdl), потом рефлексия проворачивается через мясорубку редюсеров, которые накидывают свои навороты (в том числе, там обрабатываются пользовательские директивы серверной стороны), и в итоге всё это маппится на схему типов webonyx. Получившаяся схема-webonyx закидывается в исполнитель-webonyx, который уже обрабатывает пользовательские запросы согласно схеме.
                                                                        0
                                                                        Может быть если схема генерировалась из типов в приложении, то этих недостакков небыло бы?
                                                                          0
                                                                          АПИ — это вью, типы в системе — это модели. В нормальном мире они не должны быть связаны, т.к. изменение внутренней логики не должно аффектить представление.
                                                                            0
                                                                            мм, есть DTO для API, и есть Entity/модели, они не связаны. И из DTO можно генерировать GraphQL типы.
                                                                              +1
                                                                              Согласен. Только чем лучше это DTO нативного GraphQL SDL выражения? Те же утки, только в профиль.
                                                                                +2
                                                                                Эти DTO описаны ЯП приложения, т.е. там есть генерики, а по эти можно генерировать GQL схему.
                                                                                  0
                                                                                  т.е. там есть генерики

                                                                                  это не точно )


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

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

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

                                                                            +1
                                                                            Можно пример или где это описано для OpenAPI?
                                                                              0
                                                                              не совсем понятно почему OpenAPI, стандарт описания API, должен диктовать как пользователю описывать какой-либо параметр?
                                                                              Никто же не мешает сделать что-то вроде:

                                                                              parameters:
                                                                              - in: "query"
                                                                              name: "fields"
                                                                              type: "string"
                                                                              description: "Comma-separated list of fields to return"
                                                                          0
                                                                          Главным образом, потому что хотелось единожды написать API под всех возможных потребителей. В случае с OpenAPI/JSON-API и проч. пришлось бы заниматься поддержкой нескольких разных схем для разных потребителей.
                                                                          0
                                                                          1.2. INPUT

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

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

                                                                          4. Дженерики

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

                                                                          Зачем это нужно? Никто не пишет схему руками — для большинства языков программирования есть DSL для GraphQL с дженериками на уровне этого языка и всеми сопутствующими плюшками.
                                                                            0
                                                                            Когда, например, вы в любой ORM в метод «save» передаете объект как аргумент, вы же не можете каким-то образом «не передать» часть атрибутов этого объекта

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

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


                                                                              input ExampleInput {
                                                                                value: Int = 0
                                                                              }

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


                                                                              input ExampleInput {
                                                                                value: Int
                                                                              }

                                                                              как


                                                                              input ExampleInput {
                                                                                value: Int = null
                                                                              }

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


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


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

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

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

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

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

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

                                                                                  Ok, у нас есть обьект с 10 полями, как быть если мы хотим обновить только 5 из нихЁ остальным не меняя значение. Создавать по фуникции для каждой комбинации?
                                                                                    0
                                                                                    У меня есть решение, но придется обождать пока допишу материал )
                                                                                      0
                                                                                      Предлагаю вместо абстрактных рассуждений рассмотреть конкретный пример.
                                                                                      Скажем, есть такой тип:

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

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

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

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

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

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


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

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

                                                                                                  0
                                                                                                  Ок, будет интересно почитать) Только хотел бы попросить кое-что, если не трудно — напишите, пожалуйста, в будущей статье, в чем основной профит от предлагаемого подхода. Потому что я на 90% уверен, что в GraphQL partial update реализуется только усложнением входных типов. А делать partial update ради partial update, либо бороться против 2Кб дополнительного трафика (или использовать это как основное средство против race conditions) выглядит не очень хорошей мотивацией)
                                                                                                    +1
                                                                                                    Смысл partial update в том, что пока Вася обновляет описание сущности, Игорь добавляет туда фотографии или меняет цену, или название. Если обновлением перезаписывать все поля, то есть большой шанс откатывать таким образом чьи-то изменения.
                                                                                                      0
                                                                                                      Вы всерьез верите, что partial update — хорошее средство защиты от перезаписи при одновременном редактировании несколькими пользователями? :) Если Вася и Игорь одновременно загрузили страничку с формой обновления сущности, Вася поменял описание и цену, а Игорь поменял название и исправил опечатку в описании, то partial update никак не защитит от того, что Игорь перезапишет все изменения Васи в поле описания сущности. В таких случаях применяется что-то более нормальное — например, optimistic lock.
                                                                                                        0
                                                                                                        optimistic lock
                                                                                                        И как вы его примените в этой ситуации, позвольте спросить.
                                                                                                          0
                                                                                                          Так, как это обычно и делается в самом классическом его применении — мы увидим, что в изменениях Игоря есть конфликты и покажем ему предупреждение \ выкинем исключение, etc. Уточните, какой именно момент непонятен.
                                                                                                            0
                                                                                                            Как мы узнаем, что есть конфликты? Вася открыл форму редактирования и ушёл к начальству уточнять вопрос. В это время Игорь отредактировал пару полей. Через 10 минут Вася приходит, редактирует поля в форме и сохраняет их — на сервер идёт запрос и обновляется сущность целиком — Вася не знал, что пара полей уже изменилась. А бэк подумал, что Вася изменил больше полей, чем это есть на самом деле. В итоге изменения Игоря откатились по факту.

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

                                                                                                            Upd А, кажется, понял. Отправлять не только новые значения, но и старые — чтобы сервер сравнивал, совпадают ли эти старые значения, перед обновлением. Мороки это, конечно, добавит знатно ;)
                                                                                                              0
                                                                                                              Эх… Если Вася открыл форму и ушел гулять, а Игорь в это время что-то сохранил, то Вася потом ничего не затрет, а, наоборот, получит алерт. Почитайте, пожалуйста, про принципы, на которых реализуется обычно optimistic lock — версионность и т.п., как это сделано в разных Wiki, в issue tracking системах типа Redmine и других.
                                                                                                                +1
                                                                                                                Это мы на ровном месте усложняем наше приложение — как в разработке, так и в конечном интерфейсе. Васе и Пете не нужна версионность, им просто нужна возможность совместно управлять контентом, к примеру, интернет магазина. Partial update это обеспечивает в большинстве случаев, без дополнительных затрат.
                                                                                                                  0
                                                                                                                  Приводите, пожалуйста, аргументы или какую-то статистику к своим выводам, а то обсуждение получается немного бесполезным.
                                                                                                                  Optimistic lock это просто и поддерживается некоторыми ORM вообще из коробки. Надеяться на partial update как на основное решение при совместном редактировании считаю непрофессиональным. Где-то оно, может, и будет помогать худо-бедно, а где-то, где, скажем, поле «описание» у товара редактируется в 10 раз чаще других полей — не будет.
                                                                                                                    0
                                                                                                                    Пожалуй, возьму паузу. Надо бы поподробней посмотреть на реализации. Из коробки я помню подобное только в эластик серче — автоматически создаётся новая версия при апдейте.
                                                                                                    0
                                                                                                    UPD. Тут был спойлер, но больше его нет. Простите, больше не буду
                                                                                                      0
                                                                                                      Да ну тебя, Кир… всю интригу развалил…
                                                                                                      Хотел статью написать, а теперь придется в Доту играть (
                                                                                                        0
                                                                                                        Fixed
                                                                                                          0
                                                                                                          Воу-воу, а не видел, что там было написано. Так что пишите статью, мы все её ждём :)
                                                                                    0
                                                                                    Обычно на любую проблему есть решение. Бывает костылят, бывает хорошие решения. Знатоки в этой области предоставили бы решения(е) на эти проблемы
                                                                                        0
                                                                                        Извините, что так поздно отвечаю. Batch API — по сути очень похоже на JSON-RPC. Partial response — отлично работает с плоскими сущностями. С многомерными сущностями, наверное, тоже можно что-то придумать. То есть, из этого всего, при должном усердии, можно слепить что-то похожее на GraphQL.
                                                                                        0
                                                                                        Удивительно, никто не упомянул то, что изначально было selling point для GraphQL (во всех первых презентациях об этом было) — фрагменты.

                                                                                        Собственно, GraphQL — это удобный контракт между бэкендом и фронтендаом, а для SPA с компонентным подходом — фрагменты, лежащие рядом с UI-компонентами сильно упрощают поддержку приложения.

                                                                                        Как бы весь дизайн GraphQL идет от нужд фронтенда в первую очередь, поэтому чистые бэкендеры все его плюсы и не видят.
                                                                                          0
                                                                                          В типизированных языках, сами фейсбуковские девелоперы рекомендуют Code first подход при создании схемы. Это решает небольшую часть описанных проблем для программиста. Однако, конечно же, это не спасает пользователя от страшной схемы с длинными именами вместо неймспейсов, десятков типов описывающих одно и то же вместо генериков и т.п.
                                                                                            0
                                                                                            У меня на проекте профитен был обратный подход. Вначале пишется схема, накидываются автоматические моки и утверждается. А затем уже идёт разработка, как с фронта, так и с бека по готовой схеме. Параллелить таким образом задачи получается отлично.
                                                                                              0
                                                                                              У нас в итоге так же получилось, хоть и на джаве

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

                                                                                          Самое читаемое