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

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

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

  • Верно ли я понимаю, что речь идёт именно о некоем аналоге SQL?
  • Что такое «полностью статическое клиентское React-приложение»?
  • Существует ли какая-нибудь готовая теория (или фрагмент теории) запросов описываемого вида?


Было бы любопытно взглянуть на архитектуру базы данных (или, даже, следует говорить уже об архитектуре системы управления базами данных?), которая оптимальным образом соответствует семантике GraphQL.
НЛО прилетело и опубликовало эту надпись здесь
Верно ли я понимаю, что речь идёт именно о некоем аналоге SQL?

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


Что такое «полностью статическое клиентское React-приложение»?

Знаю React поверхностно, так что могу быть неправ. Скорее всего автор имеет ввиду создание статических страниц (наподобие GitHub Gist), причём Gatsby собирает их на компьютере разработчика, делая запросы к БД при помощи GraphQL, собирая всё так, что при работе сайта БД уже не требуется, так как все необходимые данные были получены при сборке и были интегрированы в саму страницу.


Существует ли какая-нибудь готовая теория (или фрагмент теории) запросов описываемого вида?

Не понял вопроса. Теория – сама эта статья, есть примеры кода, есть ссылки на учебники, руководства. Или вы имеете ввиду что-то другое?


взглянуть на архитектуру базы данных

GraphQL не привязан к структуре базы: в базе данных может не существовать полей author или commentsCount – согласно примеру, посты могут храниться в postgres, поле commentsCount будет каждый раз подсчитываться:


SELECT COUNT(*) FROM comments WHERE post_id=$1

Ещё какие-то вещи могут хранится в Redis и т.д., а GraphQL обеспечивает единый механизм доступа к этим данным и необходимую вложенность

Спасибо. Кое-что стало яснее.
НЛО прилетело и опубликовало эту надпись здесь

Хороший вопрос.
Решение "в лоб" – ограничить уровень вложенности.
Более сложное – вычислять циклы, как это делает nodejs, когда вы делаете


> a = {number: 1}
> a.me = a
> console.log(a)
{ number: 1, me: [Circular] }

То есть просто не отдавать посты для автора, для которого посты уже были отданы

НЛО прилетело и опубликовало эту надпись здесь
Решения существуют, конечно. Вы же не первый, кто с этим столкнулся. Например: github.com/pa-bru/graphql-cost-analysis, github.com/slicknode/graphql-query-complexity, github.com/4Catalyzer/graphql-validation-complexity, github.com/stems/graphql-depth-limit
Для этого нужно не давать клиенту выполнять произвольные запросы, а только разрешать заранее определённые через их ID
Спасибо
Предположим у вас интернет-магазин с graphql, и приходит запрос:

categories {
id
name
products {
id
name
picture
price
description
comments {
id
author {
id
city
name
}
message
}
}
}


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

То есть, если вы готовы, по-сути, отдать базу сайта всем желающим, то graphql вам с этим поможет.

Да, вы правы, с GraphQL действительно вытащить всю базу проще, чем используя, например, REST, где пришлось бы делать по запросу на категорию, потом по запросу на каждый товар и т.д.
Но и используя REST возможно это сделать. Как решается эта проблема? Самое простое решение – ограничивать число запросов с одного IP за какое-то время.
В случае GraphQL такое сделать сложнее, но всё же возможно: указывать максимальный limit для товаров и т.д. Я думаю что с распространением GraphQL будут разработаны методы защиты от таких атак.


Также, помня что эта технология была разработана Facebook, можно посмотреть как они борются с подобным (и борются ли вообще?)

С трудом они борятся, по крайней мере на их сайте instagram.com, где grapql используется для ajax запросов, с помощью него можно вытащить за один запрос в разы больше информации, чем через официальное rest api. Последнее время они ввели лимит на время обработки запроса, но по-мне это крайне хреновое решение, чреватое для реальных пользователей.

Например, если вы хотели получить список публикаций по хештегу, а также комментарии к ним и профили комментаторов, то вам надо было:
запрос на rest api возвращающий список публикаций по хештегу
запрос на rest api на каждую публикацию возвращающий список комментариев
запрос на rest api на каждого комментатора возвращающий его профиль

через graphql это решается одним запросом

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

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

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

Лично мне GraphQL нравится скорее за возможность указать какие поля в ответе нужны клиенту.
В реальном приложении я бы использовал ограничение количества документов и уровня вложенности: не более 2-3 уровней, я считаю хватит для большинства приложений, тем более если раньше вы работали с REST API, в котором уровень вложенности зачастую только один.

Вам это нравится как фронтэнд или как бекэнд разработчику?

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

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

Парсинг можно отдать внешней библиотеке.
Проверять вложенность можно также доверить библиотеке или написать middleware который будет этим заниматься.
Работать с БД можно используя модели, отправляя запросы не напрямую с resolver-ов, а, как это показано в примере, делать что-то вроде


Posts.find({ id: args.id });

А модель Posts уже знает какой запрос нужно отправить и т.д.
В идеале даже не запрашивать данные сразу, а возвращать некие promises, которые будут оптимизированы и выполнены с минимальной нагрузкой уже после работы resolver-ов (т.е. не отправлять 10 запросов, а отдать 10 промисов и потом, перед отправкой, объединить их в один запрос, если это оказалось возможным). Но это только мои мысли, я не знаю систем, которые сейчас работают подобным образом.


Я писал про REST имея в виду каноничный REST, который в ответе на запрос объекта возвращает только объект и всё, без вложенных объектов. Разработчики backend, конечно, могут отправить данные, но, насколько я понимаю, REST задумывался именно так. Возможно конечно что я неправ.

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

Не встречал я описания такого канонического rest-а, особенно в плане что должен возращаться только объект и всё, без вложенных объектов. Можете дать ссылку на такое описание стандарта?

То, что я не представляю как должно работать – это только мои мысли, я думал о подобном ещё когда не знал ничего о GraphQL. Можно делать и без этого, сразу получая данные в resolver-е, как и описано в статье.
Остальное – библиотека для работы с GraphQL, алгоритм проверки вложенности – да, конечно это усложнение проекта, но если вы захотите подобное реализовать для REST архитектуры, вам тоже придётся усложнять ваш проект.


По поводу канонического REST – возможно действительно нигде не описано что должен возвращаться только этот объект. И я, создавая REST backend-ы, возвращал помимо полей объекта, хранящихся в БД, другие поля, вычисленные или полученные из других объектов.
Но наверное в любом руководстве к REST в качестве примера будет один возвращаемый объект, а не вложенные.
Возможно я неправ по поводу этого пункта, в любом случае это мало относится к GraphQL.

А что можно сделать предосудительное с полученной таким образом базой данных? Если предоставляется доступ, то, наверное, ничего критичного в базе данных нет. В обычной ситуации, Вы и так получаете доступ к базе данных, получая, например, список товаров (по определённым критериям), когда что-то ищите, чтобы купить. Что же может быть такого в структуре, компрометирующее владельца базы данных?
Упрощение жизни скраперам, которых почему-то никто из делающих бизнес в инете не любит.
Расскажу из жизни маркетплейса: все маркетплейсы друг-друга выкачивают, чтобы мониторить ассортимент и цены конкурента. При этом, если не душить того, кто к тебе приходит с фул-сканом, он с удовольствием выжрет все твои ресурсы (а что, ещё и лёгкий DoS).

Так что, на то, чтобы конкуренту было не удобно тебя выкачивать «по-быстрому», прилагается не тривиальный объём усилий.

А потом ещё веб-краулеры заявляются, и тоже хотят тебя проиндексировать. Если в коде веб-приложения затесался тяжёлый запрос, краулер с радостью тебя приложит.
Всё это можно реализовать.
Хотите заверните в JWT, генерите сессию, разные query, ограничение на ip… и ещё масса вариантов.
Всё работает.
Очень даже приятный продукт.
Недостатки есть, но плюсов однозначно больше.

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

Когда бэк умеет отдавать то, что не нужно фронту — это путь к проблемам.

Backend будет отдавать только то, что вы его "научите" отдавать.
Frontend будет запрашивать только то, что ему нужно для отображения пользователю.

Насколько я понимаю, смысл технологии в том, чтобы научить его значительно большему количеству вещей, чем необходимо в моменте. И потом каждый раз не переучивать.
Подводные, очевидно, в потенциальных рисках по безопасности, лишней нагрузке на бэк и удешевлении кражи контента конкурентами.
НЛО прилетело и опубликовало эту надпись здесь
Ключевое слово — «удешевление». Аргумент не основной, но важный. Часть стоимости сайта — его база. Может ещё на гитхаб дампы выложить, ну чтобы сервера не мучать?
НЛО прилетело и опубликовало эту надпись здесь
По поводу этапов — найдутся люди, у которых есть еще и четвертый — «Я так и не успел изучить эту библиотеку, но она уже устарела и у нее есть три замены».

Упоминая GraphQL никто никогда не говорит о минусах, что очень странно:
1) Нет ничего, кроме жёсткого сравнения. Никаких "больше" или "меньше".
2) Нет описания ограничений запросов (лимиты и пагинация). Область ответственности ложится на Relay и прочие штуки. Только Relay реализация пагинации — это феерический трешак.
3) Нет возможности описать полиморфичные типы, например коллекцию A, которая может возвращать либо B, либо C. Т.е. нельзя описать тот же объект пагинации, данные внутри которого могут быть коллекцией из других сущностей.
4) Нет возможности совместить один элемент и коллекцию. Надо писать два эндпоинта (query) для получения коллекции, например "коллекции юзеров" и "одного юзера".
5) Любое обновление данных (mutations) — отдельный эндпоинт, т.е. для каждого "типа" обновления — отдельные точки входа. Т.е. в случае обновления данных — REST на несколько порядков удобнее.


Это то, что сходу вспомнилось.

Отвечаю с точки зрения GraphQL, а не конкретной библиотеки


  1. Эта проблема однозначно решаема. Либо расширением спецификации, чтобы было возможно писать


    query getMyPost($fromId: Int, $toId: Int) {
      post(id: {ge: $fromId, le: $toId}){
        title
      }
    }

    примерно как это реализовано в mongodb
    либо даже без изменения спецификации:


    query getMyPost($fromId: Int, $toId: Int) {
      post(fromId: $fromId, toId: $toId){
        title
      }
    }

  2. Лимиты и пагинация (видимо имеется в виду указание offset-а) могут быть также реализованы через аргументы и ограничены на backend-е до нужных значений (чтобы нельзя было запросить сразу миллион документов). Ну и документ по ссылке из сообщения VlastV


  3. Также VlastV дал ссылку, там вроде то, что нужно


  4. Можно создать endpoint user и при запросе без аргументов отдавать всех пользователей (не больше определенного количества конечно), при указании id выдавать конкретного пользователя, при указании списка из нескольких id или ограничений (from..to) пользователей с соответствующими id.


  5. Можно сделать один endpoint, через него получать данные, через него же и устанавливать (аргумент set, например, будет устанавливать новое значение для поля). Это плохое решение, лучше разделять получение данных и установку (и запросы GET/POST), но технически можно сделать даже одним endpoint-ом.

А я не спорю что решений нет, решения есть по всем вариантам. Но они костыльные все. А от пагинации в Relay стиле хочется плакать =)


Ну кроме п.3 — почему-то проглядел этот пункт в доках, мой косяк.


По поводу мутаторов — хотелось бы что-то более профитное, например:


query u {
  users(id: [1, 2, 3]) {
    id
    login
    is_active = true;
  }
}

Получаем 3 юзера по их ID и активируем их.

P.S. Пагинацию для себя сделал по другому, в таком виде:


query {
  users {
    id
    login
  }

  query2: users(_limit: 10) {
    id
  }

  paginate(query: "users") {
    total_count
    pages_count
  }

  paginate(query: "query2") {
    total_count
    pages_count
  }
}

Я бы всё-таки разделял получение данных и изменение: получать GET запросом, менять POST запросом. Так, мне кажется, безопаснее.

Никто не мешает, к примеру, запрещать запись если запрос передаётся через GET.


Я просто озвучил один из вариантов, когда было бы на порядок удобнее, по-моему, т.е. именно так как мне хотелось бы чтобы оно работало. Жаль, что такой вариант не прокатит. Это требует модификации спецификации, а не простой надстройки над ней.

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

@SerafimArts, разве такая конструкция не решает вашу потребность?


mutation BatchUpdateProfile($id: [ID!]!, $profile: ProfileInput!) {
  batchUpdate(id: $id, profile: $profile) {
    id
    name
  }
}

input ProfileInput {
  active: Boolean!
}

Ну…
1) Это слабочитаемо
2) Это всё же мутатор "batchUpdate" который придётся писать на сервере
3) Надо дублировать все селекторы из запросов в этот мутатор


Т.е. на сервере дофига копипасты. С таким же успехом можно использовать тот же самый rest — он будет проще, возможностей больше и меньше писанины на сервере.


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

Я вот всё никак не пойму — первый пример (со списком полей) действительно настолько проблемный? Почему бы не указать нужные поля в качестве GET параметра, например: GET /api/v1/posts/?fields=author,text,likes.avatar.
Наша библиотека для построения REST такого из коробки не умела, но через пару часов был готов миксин с неограниченным уровнем вложенности.

Похожий подход используется в YouTube API кстати. Но видимо Facebook было этого мало.
Вообще, если смотреть на GraphQL и на такой способ определения необходимых полей, то создаётся впечатление, что это – урезанная версия GraphQL, ну или адаптация его под REST. Возможно что только у меня.
Ну и этот вариант не умеет в именованные запросы, параметры, типизацию и другое что умеет GraphQL

Я вот всё никак не пойму — первый пример (со списком полей) действительно настолько проблемный

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

/get.php?q=select_*_from_users_left_join_comments_on_comment.userid=users.id
NographQL, скоро на экранах. Тёплый, ламповый, твой.
Уже не раз сталкивался с этой проблемой, разработчики не знают что такое REST и из-за этого делают уродливые апи с кучей лишних вызовов.
REST — это архитектура предпологающая клиент-серверное взаимодействие, БЕЗ хранения промежуточного состояния. Нет состояния, нет проблем, есть куча фишек вроде кеширования и слоистости. Это не про ресурсы /user:id user/:id/comment/:id, не про вербальность GET-PUT-DELETE… как думают многие и откуда растёт миф:
Пользоваться старой REST-моделью это как заказывать пиццу, затем заказывать доставку продуктов, а затем звонить в химчистку, чтобы забрать одежду. Три магазина – три телефонных звонка.

REST не запрещает тебе сделать метод POST /doMagic {pizza: {}, address: {}, tea: {}}, и если ты пошлёшь все необходимые данные, то он закажет пиццу, доставку и включит чайник. Отличие от классического подхода то, что ты не установил адрес доставки при регистрации пользователя, а при запросе взял его сс сервера, а отправляешь его явно на /doMagic в виде сырых данных либо идентификатора по которых его можно получить из стораджа (вреде userId).

Это к тому, что GraphQL тоже является REST подходом, просто у него апи не VERB resource/:id и вам никто не мешает делать такое апи. Например мы, посылаем декораторы, чтобы сказать какие поля получить
GET /user/:id?decorators=name,address,comments.count. Не придумывайте себе исскуственных ограничений, которые не описаны в настоящем REST.

REST обязывает выполнять запросы на получение данных через GET, создание и обновление через POST/PUT/PATCH, удаление через DELETE. GraphQL же может нарушать это, так что нет, GQL — это не REST.


P.S. То что вы описываете как REST — всего лишь определение для Stateless (да, REST обязывает сервис быть stateles, но не ограничивается только этим).

Да, для REST есть ограничения, чтобы считаться RESTful, но среди них нет ничего про заголовки, урлы и т.д., что является лишь одной из реализаций, но не обязательным условием. Да и заголовков, урлы — это протокол передачи данных, это HTTP нужны метод, ури, заголовки… Вместо http может быть другой протокол, это одно из требований слоёностии REST.

На счёт преимуществ станадрта для подобных контрактов (Ваших decorators).


Вам достаточно привести пример REST апи запроса, когда один http-запрос должен возвращать:
1) 10 пользователей
2) Все доступные права
3) У каждого пользователя должны быть id + дата создания
4) Дата создания в формате RFC3339


Пример на gql:


{
    users(count: 10) { id, createdAt(format: RFC3339) }
    permissions { name }
}

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

Я согласен, что стандарт — хорошо. Ещё одно стандартное апи помимо ресурсооринтированного это здорово, лучше, чем изобретать самому декораторы. Но это не делает его не REST-ом, например можно использовать прокси-кэш.
Использование GraphQL поверх REST

Не поверх, а вместо.
Почему «вместо», когда GraphQL вполне себе REST?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации