Комментарии 23
GET /flavors
{
"fields": ["name", "description"]
}
Такое уже есть и вполне себе применяется в некоторых местах, зачем городить что-то, когда проблема успешно (вроде) решается?
GraphQL, на мой взгляд, честнее и удобнее делает то же самое.
Механизмов много и на самом деле к каждому полю можно сделать `resolver` в котором можно решать и проблемы доступа и прочее, нет весь запрос не обломается
На мой взгляд в статье стоило затронуть вопрос о том, как быть, если клиент попробует запросить у сервера сразу все данные. Тем паче, что graphql (если разработчик приложения об этом позаботился) неплохо умеет такие ситуации разруливать.
Flavor: {
nutrition: (parent) => {
return mongodb.collection('nutrition').findOne({
flavorId: parent.id,
});
}
}
findOne
Жуть какая. А можно там как-то сделать поиск по nutrition для списка родительских объектов? Чтобы было не 1+M запросов, а ну хотя бы два?
Типа вот medium.com/slite/avoiding-n-1-requests-in-graphql-including-within-subscriptions-f9d7867a257d
Причем эта особенность рубит на корню использование GraphQL где-либо кроме сайтика на 10 человек. В статье героически ускорили с 20 секунд до 200 миллисекунд, а это всего лишь один подзапрос, а таких могут быть десятки. Не говоря уже о том, что ORMки для реляционных баз как минимум все 1 к 1 склеят в один единственный запрос, а тут их всё равно будет много.
> Причем эта особенность рубит на корню использование 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 проблемой, либо разбирайте запросы руками.
Подробности о GraphQL: что, как и почему