Pull to refresh

Comments 23

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

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


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

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

В GraphQL просто стандартизирован синтаксис подобных запросов
Думаю, тут основная фишка в том, что бизнес-логика работы с данными переносится на клиента. Сервер только предоставляет некий АПИ для выполнения произвольных (но ограниченных схемой) запросов. Хотя, я могу и ошибаться.
UFO landed and left these words here
UFO landed and left these words here
Авторизация как хотите через токен, куки или еще что — вам решать
Механизмов много и на самом деле к каждому полю можно сделать `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 проблемой, либо разбирайте запросы руками.

Only those users with full accounts are able to leave comments. Log in, please.

Information

Founded
Location
Россия
Website
ruvds.com
Employees
11–30 employees
Registered
Representative
ruvds

Habr blog