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

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

Боюсь, что меня заминусуют, но из статьи непонятно, чем это принципиально лучше, чем существующий подход, когда можно отправить запрос типа
GET /flavors

{
"fields": ["name", "description"]
}


Такое уже есть и вполне себе применяется в некоторых местах, зачем городить что-то, когда проблема успешно (вроде) решается?
НЛО прилетело и опубликовало эту надпись здесь
Есть еще multipart/batch запросы. Только всё это уже какой-то не true REST. Нужен отдельный эндпоинт, понимающий только POST. И нам же надо делать зависимые запросы, чтобы получить поля связанных сущностей, и у PayPal такая возможность заложена, но тогда получается что эндпоинты ресурсов получают идентификаторы предыдущих запросов, а это уже не stateless.

GraphQL, на мой взгляд, честнее и удобнее делает то же самое.
Это лучше тем что выборка полей динамическая и можно за один запрос взять с сервера все необходимые коллекции/свойства с вложенными полями. Как если бы нужно было взять с сервера все фильмы за последние 5 лет, но только названия и длину фильма. И сразу взять список всех актёров, но только имена без фамилий… А ещё можно у актёров взять сразу фильмы в которых играли они и тоже только названия и/или длину, а у этих фильмов ещё и список актеров (и т.д.) почти до бесконечности. Дерево вложенное в дерево.
Такой запрос можно и без GraphQL сотворить аналогично тому что выше:
GET /movies?fields={name,actors.name}&release_date_from=2014-04-03

В GraphQL просто стандартизирован синтаксис подобных запросов
Думаю, тут основная фишка в том, что бизнес-логика работы с данными переносится на клиента. Сервер только предоставляет некий АПИ для выполнения произвольных (но ограниченных схемой) запросов. Хотя, я могу и ошибаться.
НЛО прилетело и опубликовало эту надпись здесь
Вы GET-запрос отправляете с телом?
НЛО прилетело и опубликовало эту надпись здесь
Авторизация как хотите через токен, куки или еще что — вам решать
Механизмов много и на самом деле к каждому полю можно сделать `resolver` в котором можно решать и проблемы доступа и прочее, нет весь запрос не обломается
Об обработке ошибок в этой статье почему-то вообще ничего не сказано. Вообще в GraphQL ответ может содержать поля data и errors, причем одновременно тоже можно. Таким образом можно вернуть данные и одновременно сообщить об ошибке доступа к какому-либо полю, если хочется.

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

Flavor: {
  nutrition: (parent) => {
    return mongodb.collection('nutrition').findOne({
        flavorId: parent.id,
    });
  }
}

findOne

Жуть какая. А можно там как-то сделать поиск по nutrition для списка родительских объектов? Чтобы было не 1+M запросов, а ну хотя бы два?

Не видел ни одной статьи, где описывалось бы как это нормально сделать. Предлагают только вместо выполнения одинаковых запросов складывать их в некий батчер, который должен догадаться, что вот это надо бы кучкой выполнить. И пляски с бубном как это прикрутить к QraphQL чтобы он и не заметил.
Типа вот medium.com/slite/avoiding-n-1-requests-in-graphql-including-within-subscriptions-f9d7867a257d

Причем эта особенность рубит на корню использование GraphQL где-либо кроме сайтика на 10 человек. В статье героически ускорили с 20 секунд до 200 миллисекунд, а это всего лишь один подзапрос, а таких могут быть десятки. Не говоря уже о том, что ORMки для реляционных баз как минимум все 1 к 1 склеят в один единственный запрос, а тут их всё равно будет много.
Да нет там никаких танцев с бубнами. В GraphQL резолвер может вернуть Promise. То как именно вы группируете запросы и в какой момент резолвите — для GraphQL не важно.

> Причем эта особенность рубит на корню использование GraphQL где-либо кроме сайтика на 10 человек

Сильное утверждение. Но, конечно, ложное. Тот же бэкенд фейсбука вполне себе живет на GraphQL + Dataloader.

> Не говоря уже о том, что ORMки для реляционных баз как минимум все 1 к 1 склеят в один единственный запрос, а тут их всё равно будет много.

В реальности GraphQL + Dataloader (который в статье упоминается) также объединят в один запрос. А в некоторых случаях — еще и эффективнее (например, когда одинаковые сущности на разных уровнях запрашиваются).

Ну и надо помнить, что Dataloader'ом легко оборачивать и соединять разные источники данных (кэш, search engine, база, web-service), в отличие от ORM.

В общем на практике все это вполне себе хорошо работает. Проблема N+1 там решается неплохо.
И как оно предполагается работать для склеивания?
То, что упоминается в статье, очевидно вызывает лоадер для дочерних коллекций после того, как был загружен родитель, иначе ему просто нечего передать как параметр хоть и в промис. В такой схеме даже теоретически невозможно поклеить всё в один запрос, только если самому всё распарсить, но собственно зачем тогда нужен QraphQL?

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

Даже в упомянутом Dataloader в описании пример с юзером, топ 5 друзей и лучшим другом у каждого юзера, где обещают не более 4 запросов (вместо 13), когда ормка превратит это в 1 или 2, в зависимости от реализации 1 к N.

Про кэши и ORM вообще не понял, причем тут они. Кэши прикручиваются куда угодно без особых проблем, паттер репозиторий в помощь или что угодно другое. Ормка лишь источник данных, кого дергать это проблемы конкретного проекта и конкретного случая.
Тот же бэкенд фейсбука вполне себе живет на GraphQL + Dataloader.
Можно пруф?
Dataloader'ом легко оборачивать и соединять разные источники данных (кэш, search engine, база, web-service)

Фактически, это значит, что в кэше можно хранить данные из нескольких функций — и это уже не такое же смелое заявление, как в исходной форме.
У меня, к слову, вот такой DataLoader-подобный кеш на промисах в проекте крутится в одном-двух компонентах — как жаль, что я не фейсбук и пиарить свои isFunction так же хорошо не умею, ведь про мой кеш даже в моём проекте мало кто знает.


в отличие от ORM

Назначение ORM — оградить бизнес-логику от знаний, как именно хранятся данные. Поэтому, достаточно развитые ORM позволяют точно так же объединять разные источники данных — с вас потребуется только определение PersistenceUnit'а (переводчика из диалекта QCRUD ORM на диалект, нативный для Store). И работать будет уже не только для загрузка, но и запись.


Выглядит примерно как по ссылке ниже. Ограничения, по сути, те же самые, что у вашего DataLoader'а — N + M(>=N) запросов для чтения, при записи всплывает поддержка распределённых транзакций, что есть не у всех Store'ов. Но у модной нынче модели EventualConsistency и транзакций-то, по сути, нет, так что это вроде как уже не большой минус.
https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Advanced_JPA_Development/Composite_Persistence_Units

В GraphQL резолвер может вернуть Promise

А на вход он может принять коллекцию вместо одного объекта? Если нет — то выходит, что новая крутая технология в очередной раз способствует уменьшению производительности клиентских приложений. Потому что если вы посмотрите хорошо на мой вопрос — конкретно на то, что я цитировал — то выходит, что GraphQL на вложенных запросах как раз задыхается. Во-первых, потому, что в исходной цитате нету никакой проекции — значит это, что мы читаем всё, а умный GraphQL потом выбрасывает прочитанное? Но мне не нужен целый GraphQL чтоб из объекта свойства поудалять, понимаете? Или же в статье всё-таки представили технологию без раскрытия реальных киллер-фич? Во-вторых, если не пользоваться хаками EventLoop'а, как это делают в DataLoader — читать предлагается строго по одному элементу, чего я ну никак не ожидаю от действительно зрелой технологии, заточенной на оптимизацию доступа к данным.
Смотрите. Если при запросе


flavors {
  id
  name
  description
  nutrition {
    id
    sodium
  }
}

В процессе выполнения какая-нибудь подсистема получит внутренний запрос в виде


nutrition(id: $.flavors.id) { /*это JSONPath такой, надеюсь суть ясна*/
   id
   sodium
}

То я возражать не буду — многие так сделают, решение неидеальное, зато гибкое. А если такого нет, и грузить буду по одному nutrition, то последует мой вывод — "съешь ещё этих мягких калифорнийских фреймворков да выпей смузи, а клиенту скажем оперативы побольше купить".

> А на вход он может принять коллекцию вместо одного объекта?

Я делал реализацию с таким подходом (когда на входе коллекция), но у нее есть свои грабли — иногда у вас тип может быть в какой-нибудь generic обертке и у вас будет список оберток, а не конечных сущностей (В GraphQL типичный пример — Pagination and Edges).

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

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

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

Как напишите. Напишите с асинхронной буферизацией (а-ля даталоадер) — будет работать хорошо и быстро.

> Во-вторых, если не пользоваться хаками EventLoop'а, как это делают в DataLoader

Я бы не сказал, что это хаки. Просто для оптимального результата нужно контролировать последовательность буферизации и загрузки следующего чанка (грубо говоря задавать стратегию по которой промисы резолвятся). Стандартный node не дает этого делать.

В принципе если работаешь не в node-окружении и можешь контролировать порядок исполнения промисов и добавления новых в очередь — нет там никаких хаков. Очереди и стратегии по их проходу.

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

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

Если нужна только оптимизация доступа к данным — используйте SQL, Lucene, map/reduce и т.п. %)

Ваш пример не совсем понял. С DataLoader'ом конечно там будет один запрос для всех nutrition.

В вашем примере будет следующее:
1. Резолвер очередного nutrition добавит id родительского flavor в буффер даталоадера и вернет промис
2. GraphQL таким образом пройдет по всем flavor и получит у себя массив промисов для nutrition.
3. Когда не останется данных для синхронного обхода — будет ждать резолва промисов.
4. Тут в буфере DataLoader будут все flavor_id, и он выполнит один запрос для получения всех nutrition (в SQL что-то вроде `SELECT * FROM nutrition WHERE flavor_id IN (?);`)
5. По окончании — DataLoader зарезолвит все промисы для nutrition (вот здесь надо контролировать стратегию — либо GraphQL будет продолжать свой Loop после каждого зарезолвленного промиса, либо подождет, когда зарезолвятся все и будет уже проходить по всем новым данным. Все «хаки» DataLoader'а — чтобы обеспечить второй вариант)

Ну собственно это и иллюстрирует основную проблему GraphQl, ODATA и прочих "да ну его нафиг этот сервисный уровень" подходов. Мы отдали клиенту право запрашивать все что он хочет. На практике это выливается в одну из трех альтернатив:


  • либо в написание довольно сложного транслятора, который умеет транслировать GraphQl запросы в наш бэкенд (предотвращение 1+M, кэширование, аффинность, индексы, вшивание проверки доступа и т.д.)
  • либо в написание 100500 вирутальных неявно связанных "вьюшек" в схеме (привет, REST),
  • либо в перекладывание ответственности на клиента (в жестком виде — 0.5 сек на запрос и дальше получи таймаут, или в более дружественном — мониторинг и уговаривание клиента поменять кривые запросы, с шантажом и угрозами если потребуется)

Скорее всего первый вариант не пойдет в качестве самописного, если вы не фейсбук. Так что реально у вас будет или middleware слой (вроде https://www.apollographql.com/) снаружи сервисов, либо прямой биндинг к БД внутри сервисов (вроде https://github.com/graphile/postgraphile), что, по сути как раз и есть альтернатива №3. В итоге, "золотая середина" — это альтернатива №2, по сути — тот же REST только сбоку.


Отдельно замечу, что прямая трансляция из GraphQl в БД не особо отличается от обмена SQL запросами, просто язык другой. Не ведитесь на обещания "GraphQl отлично работает в микросервисной среде". Ровно так он и работает — либо живите с 1+M проблемой, либо разбирайте запросы руками.

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