Как стать автором
Обновить

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

А теперь представьте, что надо выводить для каждого кандидата данные о городе проживания (название, страна, средняя зарплата и тд). Сколько раз данные о Москве будут продублированы при запросе через GQL?

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

А как задача с недублированием данных о городе решалась бы в REST подходе?

как вам захочется

Например, можно вот так:

{
  users: [ {..., "areaId": 148, ...} ]  
   areasById: {..., "148": {"name": "Москва Москва Златые Купола"}, ...}
} 

Это же лютые костыли:

  • На сервере получили из базы нормализованные данные.

  • Денормализовали их для GQL выдачи.

  • Натравили дедубликатор, получив свой, не GQL формат.

  • Отослали клиенту.

  • На клиенте натравили дубликатор для получения GQL ответа.

  • Обработали таки GQL.

  • А клиенту эта денормализация не упёрлась - он нормализует всё обратно.

Итого, в этой схеме 2 лишних звена:

  • GQL

  • Денормализация

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

  2. Денормализует клиент и в случае с REST. Тут никакой разницы нету. Чтобы в UI отобразить. То есть "клиенту денормализация не уперлась" - я не могу себе представить такого кейса. Наоброт, в конце-концов клиенту нужна денормализцаия для отрисовки UI.

Скорее как-то так:

{
  "candidates": {
    "city=mos;date>2022-02-24;vacancy=api-architect": { "found": [ "jin", "john" ] },
    "city=mos;date>2022-02-24;vacancy=CTO": { "found": [ "jin" ] },
  },
  "person": {
    "jin": { "area": "mos", ...},
    "john": { "area": "mos", ...}
  },
  "area": {
    "mos": { "name": "Москва Москва Златые Купола", ... }
  }
}

Что тут клиент будет нормализовывать - ума не приложу.

Хорошие вопросы! На первый ответили комментарием ниже про graphql compressing, а по второму:

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

Чуть проспойлерю - буквально вчера у коллеги были съёмки нового эпизода оххнных историй как мы столкнулись с этой проблемой и как её фиксили :)
Так что про эту проблему будет отдельная статья, но да, отсутствие данных можно различить от их недоступности - разруливается на уровне контракта.

Вкратце мы это фиксили так: если сущности нет или она недоступна возвращаем объект типа CandidateError с конкретизацией, что именно не так. При этом сам Candidate - это union от CandidateItem и CandidateError (то есть для конкретного кандидата возвратится либо CandidateItem , либо CandidateError), которые можно различить по __typename. Этот подход нам помог различить 2 состояния.

candidate(id: 3) { 
  __typename 
  .. on CandidateError { 
    errorType 
  } 
  .. on CandidateItem { 
    id, 
    firstName 
  } 
}

Также мы столкнулись и с другой проблемой - одна часть данных одной и той же сущности доступна, другая нет (например, ФИО можем отдать для определённой роли в системе, а контакты не хотим). Но здесь подход концептуально не поменялся - объявляем интерфейс Candidate и от него наследуем CandidateFullItem и CandidatePublicItem (второй без контактов) - и в зависимости от роли отдаём данные кандидата определённого типа, как написал выше

Будет всего 2 запроса к бы. См graphql dataloader.

Речь не про запросы к БД, а продублирование данных в ответе из-за денормализации.

Нет строгого контракта

В случае REST можно написать или сгенерировать OpenAPI/Swagger документацию - и из неё генерить клиента для фронта

Обработка ошибок через массив errors в теле ответа

А что мешает то же самое в REST сделать? Собственно обычно ошибки валидации, например, именно так и отдаются

А что мешает то же самое в REST сделать? Собственно обычно ошибки валидации, например, именно так и отдаются

Тут скорее хотелось подсветить, что REST завязан на http-протокол и активно использует коды ответа - и в ряде случаев это очень удобно, например, для мониторинга 4**/5** или понимания какой запрос можно ретраить, а какой бесполезно. GraphQL же изначально строился как protocol agnostic и на http не завязан - поэтому и ошибки будут именно в теле запроса, при том что канонически коды ответа не используются.

В случае REST можно написать или сгенерировать OpenAPI/Swagger документацию - и из неё генерить клиента для фронта


Справедливо! В случае graphQL скорее тут плюс в том, что строгость контракта максимально естетственна в силу составления схемы, единой точки правды, от которой все и пляшут, в следствие чего документация, визуализированный graphql-playground и прочие тулзы доступны из коробки.
Кстати интересно, получится ли в связке REST+OpenApi/Swagger добиться такой же строгости контракта, как и в случае с graphQL? В случае с графкл, например, у нас получилось максимально дёшево проверять контракт перед каждой сборкой и если вдруг он нарушен - гасить эту сборку. Ну и прочие очевидные плюсы типа автодополнения/линтинга/типизированности на фронтенде на основе схемы при составлении запроса

OData ещё взрослее.

В общем, минусов тоже хватает, поэтому мы бы не советовали рассматривать GraphQL как следующую ступень эволюции работы с API после REST

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

Кеширование запросов непонятно как реализовать

На поставщиках данных. Где они и должны быть, канонически

Ретраи — не очень понятно, что теперь идемпотентный запрос, а что нет, и какие запросы можно ретраить

Это для mutation, но с mutation итак много нюансов.

А не пробовали смотреть в сторону white-листов gql-запросов, чтобы немного снизить проблему, когда клиент может получить любые данные, в любом количестве?

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

Мы пытались использовать GraphQL в качестве API, но он не подошел по ряду причин:

  1. Обязательность схемы. Это очень сковывает и не дает динамически расширять атрибутивный состав сущностей на лету. У нас своя система типов, по которой мы можем узнать что именно можно получить из сущности, но мапить эту систему на схемы GraphQL - это боль. Мы частично смогли обойти ограничение по схеме через параметры, но это стало выглядеть очень страшно. Например, для получения полного наименования контрагента имея на руках ссылку на договор приходилось писать что-то вроде: {att(n:"counterparty"){att(n:"fullName"){str}}}. Но даже с этим подходом снова возникла проблема - чтобы загрузить несколько атрибутов таким образом нужно обязательно указывать псевдонимы т.к. иначе в GraphQL берется имя атрибута (в нашем случае "att", который у всех атрибутов по факту один и тот же). Пришлось городить генерацию псевдонимов ("a", "b", "c", ...) перед тем как отдать движку на вычисление наш запрос.

  2. На UI работать с результатами очень не просто если нет возможности описывать там бизнес-сущности (мы разрабатываем универсальную платформу и бизнес-сущности в основном коде UI - это непозволительная роскошь). Т.е. приходилось вручную работать с большой вложенностью результатов и бесконечными проверками на null/undefined. Lodash конечно облегчал жизнь, но даже с ним было очень не просто жить с таким API. Решение этой проблемы оказалось довольно тривиальным - перед тем как отдать результат клиенту, мы для всех объектов в дереве результатов где ключ только один возвращаем значение по этому ключу вместо объекта. Т.о. объект {"counterparty": {"fullName": {"str": "ООО Рога и копыта"}}} превращался в "ООО Рога и копыта";

  3. Как указали ранее - общение бэк <-> бэк на GraphQL довольно проблематично, а делать несколько наборов API для разных юзкейсов очень не хотелось;

  4. К GraphQL API очень напрашивались возможности, которые свойственны template движкам. Например, отформатировать полученную дату по заданному шаблону. Вернуть значение по умолчанию если атрибут вычислился в null (и много других). Стандарт этого сделать не позволяет;

  5. Отношение к сущностям и их идентификаторам в GraphQL очень поверхностное. Нам дают возможность добавлять параметры для атрибутов в query, но то что в них передается - это все уже забота разработчика и никаких оптимизаций от движка особо ждать не приходится. Наша попытка сделать универсальный атрибут для запросов любых сущностей из любых источников данных закончилась довольно грустно. Очень не хватало возможности вернуть из атрибута некоторую ссылку на другую сущность и дать движку уже самому решать как и что на основе этой ссылки загружать;

  6. Сложность. Все таки вся эта задумка с параметрами по факту нужна для одних и тех же целей - получение объекта по ID или поиск записей по другим критериям. Эти два сценария можно было бы сделать полноценной частью API с соответствующими оптимизациями от движка, но что имеем, то имеем;

  7. Оптимизация. Реализация на java при загрузке {a: abc{def}, b: abc{hig}} по факту загружала "abc" дважды. Зачем дважды грузить одно и то же в readOnly запросе? Вопрос для знатоков.

Но справедливости ради в GraphQL API есть очень весомые плюсы:

  1. Клиент точно знает тип результатов, которые он получит из запроса;

  2. Нет underfetching/overfetching проблем (как в этой статье и описывается);

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

API.get(contract_ref).load("counterparty.fullName?str!'unknown'")

а на выходе или 'unknown' или 'ООО "Рога и копыта"'

Для поиска записей:

API.query({"sourceId": "contracts"}, "counterparty.fullName?str!'unknown'")

и получаем уже массив из имен контрагентов для всех контрактов

А что думаете про HARP?

Как обычно - модные и красивые штуковины, которые далеки от реальности.

- Типа, а давайте дадим возможность клиенту запрашивать что он хочет напрямую из базы?

- Блин, так теперь надо организовать безопасность, кеширование, контроль трафика и пр.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий