Комментарии 90
Будут ли части про «Persisted Queries», ошибки, пагинацию, аутентификацию?
Скорее наоборот, HTTP базируется на принципах REST.
Если агрегации данных от нескольких сервисов нет, то можно не говорить про graphQL-сервер — запрос к сервису это обычный вызов репозитория или что-то там у вас дергают rest-контроллеры или их аналоги. По сути, можно считать graphQL-слой очень умным фильтром-трансформером в цепочке типа response = Json.serialize(graphQL.filter(userRepository.getAll(), request));
Рисунок довольно схематический, graphQL сервер отдельный совсем не обязателен, а апп-сервер (бэкенд) может сам отдавать graphQL, обеспечивая, например, прямой маппинг на SQL.
GraphQL использует систему типов для описания данных
Нормальные люди используют sdl для декларации схемы =)
schema {
query: Query
}
type Query {
users: [User!]!
}
type User {
name: String!
email: String!
age: Int!
friends: [User!]!
}
2) Это не «по принципу», а наоборот, ребята, которые внедрили SDL в GraphQL, т.е. первоисточник этих схем.
Можете, пожалуйста, кинуть ссылку на установку этого фрейма? Я вот, например хочу скачать, поставить, настроить роутинг, настроить резолверы и проч. А не пользоваться их веб мордой. ;)
Спасибо! Буду теперь знать.
Так что я бы сказал не «нормальные люди», а те, кому просто деваться некуда. Насколько я помню, реализация SDL парсеров/компилеров есть лишь на JS, PHP и
1) JS: github.com/graphql/graphql-js
2) PHP: github.com/railt/railt
3)
UPD: Посмотрел, там свой DSL, а не нативная схема, так что только два варианта. Может кто ещё знает что на других языках?
Возможность формировать структуру и объем данных на клиенте
Я правильно, что на практике сервер всё равно поддерживает предопределённые запросы и наборы полей под конкретные кейсы? Если да, то в чём тут преимущество?
Я правильно, что на практике сервер всё равно поддерживает предопределённые запросы и наборы полей под конкретные кейсы?
Не факт, зависит от реализации сервера.
Если да, то в чём тут преимущество?
Стандартизация критериев выборки. Использование:
query {
user {
id,
login,
avatar,
friends(latest: 10) { id, login }
}
}
Вместо, например:
/api/1.0/users?fields=id,login,avatar&with=friends.id,friends.login&count=friends:10
Или нечта подобного.
Типичный вариант до оптимизации быстродействия — из репозитория ORM вытягивается нужный граф (желательно с lazy) и фильтруется по запросу.
Ведь в зависимости от того, какие поля нужны, приходится сильно варьировать запрос, чтобы не убить производительность.
Архитектурно неправильно графом ходить в хранилище данных. Граф является интерфейсом (точнее поля типа данных, для которого строится схема графа) к репозиторию. Каждый тип данных (пользователь, статья и т.п.) — это репозитории, к которым обращается граф. Как эти репозитории получают данные, как они их агрегируют, кешируют — это их детали реализации.
Рассмотрите абсолютно любую бизнес-модель с позиции, что у вас есть универсальный апи-метод, позволяющий получить каждое из полей этой модели. Как это поле будет собираться — это дело самой бизнес-модели. Можно анализировать граф-запрос, но сама логика графа подразумевает, что запрашиваться будет самый разный набор полей, поэтому чаще всего эффективнее кешировать либо всю модель, либо поля модели.
При использовании графа ответственность смещается с SLA на конкретный эндпоинт (с соответствующими уровнями оптимизации запросов) на SLA доступа ко всем предоставляемым данным. Решается задача не оптимизации медленных запросов к данным, а как отдавать данные вне зависимости от того, какие там могут быть запросы.
Решается задача не оптимизации медленных запросов к данным, а как отдавать данные вне зависимости от того, какие там могут быть запросы.
Я так понял, вопрос был в том, как реализовать такой механизм и не убить производительность. Очень сложно сделать реализацию быстрой выборки чего-угодно. Вместо этого делают фиксированный набор полей, на выборку которого затачивают запрос и т.д.
Вместо этого делают фиксированный набор полей, на выборку которого затачивают запрос и т.д.
Эти технологии применяются на разных слоях приложения.
Если в модели есть сложно вычисляемое поле, то на уровне агрегации данных модели решается задача, как это поле получать быстро.
Перед графом же ставится задача отдать любое запрошенное поле модели.
В конце концов такие модели с тяжелой логикой получения данных обзаводятся слоем кеша со сквозной записью.
Если самим графом за данными не ходить, четко разделяя уровни ответственности, то этой проблемы просто нет как таковой.
Никак. Поэтому на самом деле мало кто пользуется graphql и мало кто пытается. Потому что все сводится обычно или к потери производительности, или к тому, что оборачивают rest api в этот ваш graphql. Куча компаний не умеют делать даже что-то похожее на согласованный REST, а тут уже graphQL. Не к добру это.
Ну и к тем, кто предлагает просто смапить orm с graphQL. Так обычно не работает, потому что нужна еще куча внутренних проверок, например, имеет ли пользователь доступ к этой записи. И их может быть довольно много.
Куча компаний не умеют делать даже что-то похожее на согласованный REST, а тут уже graphQL
Проблема "согласованного REST" прежде всего в том, что REST — это архитектурный принцип, который каждый толкует как хочет даже в теории, не говоря о конкретной имплементации. graphQL же чётко описанный стандарт с референсной имплементацией.
Так обычно не работает, потому что нужна еще куча внутренних проверок, например, имеет ли пользователь доступ к этой записи.
Решаемо, на разных уровнях и задачах по разному. Никто не мешает, например, в резолвер заинжектить сервис авторизации и проверять хоть запросы к хранилищу, хоть ответы.
Проблема "согласованного REST" прежде всего в том, что REST — это архитектурный принцип, который каждый толкует как хочет даже в теории, не говоря о конкретной имплементации. graphQL же чётко описанный стандарт с референсной имплементацией.
А вы знаете в чем проблема "одного единого стандарта"? Ну и да, странно звучит, вот ребята используют какой-то sdl, а в стандарте его нет. GraphQL это тоже идея, все-таки. А структуру все все равно будут делать по своему.
Решаемо, на разных уровнях и задачах по разному. Никто не мешает, например, в резолвер заинжектить сервис авторизации и проверять хоть запросы к хранилищу, хоть ответы.
Я не говорю, что это невозможно, но это означает, что вам надо будет это делать. Инжектить сервис авторизации, потом еще инжектить сервис для проверки ip-whitelist, а потом еще помнить для какой модели что использовать, а что нет. Тут просто уйма веселый архитектурных проблем, которые позволял избегать REST.
А вы знаете в чем проблема «одного единого стандарта»? Ну и да, странно звучит, вот ребята используют какой-то sdl, а в стандарте его нет. GraphQL это тоже идея, все-таки. А структуру все все равно будут делать по своему.
sdl это какой-то инструмент сбоку для описания схемы, у меня в Эликсире там тоже свой дсл, но для клиента это будет тот же самый GraphQL, по стандарту. Это не идея, можно пойти и скачать спецификацию. Спецификации REST в природе не существует.
А структуру все все равно будут делать по своему.
Язык запроса стандартизирован, так сказать, де-юре, а передача запросов и ответов по HTTP де-факто (а может и де-юре тоже, не вникал).
Тут просто уйма веселый архитектурных проблем, которые позволял избегать REST.
GraphQL легко сводится к типичным JSON REST(ish) API c "бесплатным бонусом" в виде возможности получать не все данные, которые может сервер отдать, а только необходимые. Если у вас в архитектуре есть слой, который отвечает за выборку данных из хранилища с учётом прав пользователя, его IP, и прочих метаданных запроса, сессии и т. д., то GraphQL ендпоинт будет лишь ещё одним интерфейсом к нему.
Почти всем нерешаемые проблемы графа связаны с тем, что его пытаются применить не по назначению. Возьмем, например, пагинацию.
Есть ОРМ, которая поддерживает работу с графом. Прикручиваем пагинатор к ОРМ и через этот пагинатор ходим напрямую в ОРМ. И имеем все те проблемы, которые описывают в каждой второй статье применения графа.
Как это решается через вышеописанную схему?
Создается тип пагинированных данных. Интерфейс репозиториев расширяется, чтобы они умели отдавать данные согласно новому типу данных. Как результат пагинатором ОРМ пользуются репозитории. А граф запрашивает у репозиториев данные соответствующего типа. Задача получения данных и оптимизации запросов остается на уровне модели, которую использует репозиторий для работы с хранилищем.
Я правильно, что на практике сервер всё равно поддерживает предопределённые запросы и наборы полей под конкретные кейсы? Если да, то в чём тут преимущество?
Я выделил бы два ключевых преимущетва.
У нас есть универсальный геттер для данных. На уровне описания типов бизнес-моделей описываются их поля и связи (поля с типами других моделей). На основе этих типов потом строится вся схема графа. Бонусом можно на основе внутренней логики регулировать, какие типы кто будет видеть. Если в описании схемы какого-то запрошенного поля нет (вырезано из-за отсутствия прав), то запрос с таким полем не пройдет сам по себе. Логика резолва всех указанных в графе полей спускается на уровень ниже, для которого декларируемые поля являются интерфейсом, согласно которому сервис должен уметь возвращать данные
Второе преимущество в сеттерах (мутации). С одной стороны для каждой операции бизнес-логики пишется своя мутация (как бы аналог эндпоинта в рест), но из-за вышеописанного преимущества с декларативным способом описания, методы бизнес-логики превращаются в мутации, где наборы полей описываются таким же декларативным методом. А сама бизнес-логика уходит на следующий уровень, для которого мутация становится просто интерфейсом.
В итоге поддержка апи сводится к декларации получаемых данных и декларации методов изменения данных. Сама реализация же может делаться где угодно и как угодно, пока она использует описанные в графе декларации в качестве интерфейса.
На практике часто (можно даже сказать по умолчанию для внутренних API) клиент может выбрать весь граф объектов, которы может собрать бизнес-логика в принципе.
Самый банальный пример опять таки с пользователем. Допустим у нас залогиненый пользователь и мы должны отображать его имя и аватарку в хеде страницы. В таком случаи нам нужно всего 2-3 поля, а не данные по всему интерфейсу пользователя. К тому же можно увеличить TTI за счёт того что первым запросом запрашивать данные для видимой пользователем части экрана, а остальное подгружать лениво.
Фишка GraphQL в том, что во вью (ответе сервера) всегда ровно столько данных, сколько нужно клиенту (фронту). Нужны только имя и аватарка — выбираете только их, нужны паспортные данные друзей друзей друзей — выбираете их. У вас неограниченная (логически в теории, на практике разработчики бэкенда могут ограничивать по соображениям экономии ресурсов и безопасности, да и сами вы можете не захотеть ждать результатов "семиэтажного" запроса, который выполняется полчаса) возможность обходить предоставленную через GraphQL модель.
А в плане разработки? Как часто ваши фронтендеры просят бэкендеров добавить в API то или иное (уже существующее в бэкенд-модели поле или целый граф), а то и целый новый ендпоинт, выдающий те же данные, что и другие, но в другом шейпе?
Для проектов, не испытывающих особой нужды в низкоуровневых плюшках GraphQL, могут оказаться полезными архитектурные, снижающие зависимость фронтов от бэкенда.
В общем случае это решается наличием соответствующего функционала у репозитория. Тип возвращаемых данных — это один из его интерфейсов. Используя этот тип данных репозиторий преобразует внутреннее хранение полученных из хранилища данных в стукруту, которая передается пользователю. Аналогичным образом граф у репозитория получает этот тип данных в соответствующем формате.
Ограничение доступа остаётся на уровне репозитория, который сам фильтрует, по своей внутренней логике, какие поля кому доступны. Что клиент не может получить у этого репозитория недоступные ему поля (потому что всякая выдача проходит через преобразование в этот тип данных), что граф, запрашивая схему для типа данных, получает только тот набор полей, который доступен данному пользователю.
Добавлю, что вы можете создать два типа данных (запроса), например, UserPublic и UserPrivate, что будет аналогом двух ендпоинтов. Или в резолвере одного типа User проверять права и отдавать специальные значения для полей, к которым доступа нет.
Добавлю, что вы можете создать два типа данных (запроса), например, UserPublic и UserPrivate, что будет аналогом двух ендпоинтов
Это нарушает саму идеологию графа. Запросить пользователя мы можем в любом месте любого запроса, который имеет связь с пользователем: { article { user { id } } }
Если мы будем использовать разные названия типов в зависимости от прав, то убъем универсальность доступа к данным.
Тем не менее это рабочее решение.
А уж в рамках одного из комментариев выше
В случае решения а-ля REST (т.к. это уже ни разу не REST, а JSON RPC какой-то) приходится или плодить кучу эндпоинтов на каждый чих, либо снабжать ресурс кучей get-параметров
Сам GraphQL выглядит как банальное «not invented here»
Видимо пермишны на атрибуты вешать и в fine-grained авторизацию уходить.
Если у пользователя есть пермишины — он получает полные данные, скажем так, выполняется один запрос, если нету, то другой. Это если вы хотите решать на клиенте.
Если Вы решите это выяснять на сервере, то просто сами решаете, что возвращать пользователю в зависимости от его уровня доступа, так сказать.
Система типов для описания данных, в свою очередь, очень похожа на также давно существующую JSON Schema.
Если говорить о REST как типичных HTTP RESTish API, то они идеологически плохо поддерживают не CRUD операции над ресурсами. Приходится придумывать виртуальные ресурсы типа /process/123/approve для расширения списка доступных действий над ресурсом, когда и ресурса собственно нет.
Идентификатор (URI) /process/123/approve, а по факту в модели области нет даже сущности Process, не говоря о её методе approve, только идентификатор для связи сущностей. В СУБД просто sequence без таблицы, а можно генератор айдишников сделать вообще базу не трогая.
Нет ресурса, который можно посмотреть по GET /process/123, близкое только GET /process/123/status, возвращающй коллекцию статусов связанных сущностей. Нет ничего, что можно отправить PUT /process/123 или PUT /process/123/status, только POST /process/123/, где verb — один из полутора десятка глаголов.
Сколько ни читаю посты об GraphQL, не могу понять чем он принципиально лучше существующих уже давным давно URI-совместимых query language-ей. Например, RQL или odata. Они прекрасно решают проблему выбора необходимых полей, поддерживают фильтрацию, паджинацию и все остальное. В отличии от GraphQL они не противопоставляют себя REST-у, а отлично его дополняют.
Киллер-фича графа — отсутствие необходимости версионировать апи. Из чего вытекает простота использования и поддержки точки взаимодействия клиента и приложения.
Граф противопоставляется ресту, потому что идеологически граф не общается с хранилищем, а только с репозиториями. Рест-апи в своем развитии обзаводится особыми эндпоинтами, которые получают сложные наборы данных и обзаводятся слоем логики, позволяющем получить эти данные из хранилищ наиболее оптимальным способом. И очень часто при внедрении графа возникает именно вопрос о том, как это повторить для графа. Никак. Граф не должен знать ничего о том, как хранятся данные. Он должен знать только то, какой севрис обслуживает какие типы данных.
Внезапно в данный как раз занимаюсь одним бизнес приложением с использованием этой технологии и могу много чего интересного рассказать.
На бэке стоит внезапно php и laravel для подключения используется единственная более-менее адекватный коннектор который по сути обертка над этой либой
На фронте используется React + Apollo
И так на backend находятся 25 таблиц в postgres причем 4 из них содержат jsonb поля с произвольным содержанием.
А теперь головные боли:
1) backend
Для того чтобы graphql заработал нужно описать запросы типы и мутации.
В коробке с graphql идет целая куча примитивных типов — вроде числа, строки, boolean и т.д
Далее идут структурные типы вроде объектов — причем для описания своего объекта необходимо описать ВСЕ поля объекта на основе существующих полей. в итоге 25 объектов в среднем с 8-10 полями — это ОЧЕНЬ много описаний. Но это еще не все внезапно нет готового типа для произвольного json — типы должны быть точно описаны. Но добавить свой тип в принципе возможно — пришлось добавить тип json самостоятельно. Связи указываются как поле которое ссылается на другой тип.
Хорошо у нас есть типы объектов но сами по себе они бесполезны! необходимы запросы к объектам на основе эти типов. предположим я желаю 2 типа запросов на каждый объект первый — показать по id или по другому ключевому полю и второй — показать все по каком-либо набору критерию. В итоге мы уже описываем аргументы. В итоге по 2-5 аргументов на каждую сущность в каждом запросе по два на сущность — и это опять достаточно много описаний.
Далее мутации — если взять 3 мутации (удалить, добавить, изменить) то опять описываем аргументы, для мутаций уже те самые 8-10 полей и это опять куча аргументов + еще правила валидации для каждого поля в каждой мутации.
Смотрим видим что необходимо создать просто огромное количество кода для описания graphql для CRUD+list для 25 объектов.
В итоге рвем на голове волосы от чисто объема кода и пишем некий генератор который используя рефлексию и немного doctrine/dbal шерстит наши таблицы и генерирует поля для наших типов и аргументы для наших запросов и мутаций. Сам генератор принимает список объектов и какие поля/аргументы не надо генерировать.
Все это месяц работы.
2) фронт
Здесь нам опять необходимо описать наши 50 запросов и 150 мутаций — хорошо есть генератор из php который выдаст нам схему и мы определим хитрый Higher-Order Component (HOC) который будет принимать конфиг из серии
Объект
- поля
- запросы
- аргументы запроса
- поля запроса
- мутации
- аргументы мутации
- поля мутации
После чего генерирует код наших запросов/мутаций (graphql-tag), и прописывает их в props нашего объекта.
Хорошо — теперь бы нам пробросить наши запросы в props наших объектов.
Для этого внезапно Apollo предлагает свои HOC который принимают graphql-tag и пробрасывают свойства в объект. И для контроля выполнения запросов еще один HOC.
В итоге получаем эдакий толстый бургер где наши 2 HOC это 2 булки — один для предоставления и генерации запросов, второй для того чтобы упростить вызов этих запросов а между ними уже лежит начинка в виде HOC для каждого запроса/мутации.
После чего строим 25 data-table оборачиваем их в наш HOC с разными конфигами и радуемся.
Все это звучит понятно просто и легко но по факту это достаточно сложная работа.
Итог:
- Если у вас более 5 объектов в системе и вы не хотите её делать год одному и 3 месяца одной командой из 5 человек — не используйте graphql.
- если вы не используете специальный saas/baas или другой софт который использует graphql из коробки и умеет генерировать запросы/мутации на основе типов — не используйте graphql.
- если у вас не graphqlCool фреймворк например meteor не используйте graphql
А чем бы вам помог REST? Если у вас 25 таблиц, и к каждой нужен крад, то получается вам надо написать 125 эндпоинтов, и все это с фильтрацией, валидацией, и прочим. Не думаю что это заняло бы меньше времени.
1) Я не говорю что сам протокол graphql плох
2) Имхо для описание одного REST ресурса потребуется чуть меньше времени чем на описание 1 типа 3 мутаций и 2 запросов и единственный два плюса в graphql против REST это возможность выбрать список полей (включая условные выборки) и простая выборка полей связанных объектов — собственно для чего мне он и нужен — из 25 объектов 22 можно вытянуть по зависимостям имея на руках любой из них.
3) Для использования graphql нужен отдельный сервер relay/meteor а для бэка остальных платформ кроме node.js остается только использовать Apollo клиент а эти ребята полны сюрпризов я скажу — например могут внезапно выпилить поддержку Redux в новой версии и одновременно закрыть доки на старую, плюс не допилить доки на новую в итоге документацию приходиться вытаскивать их кэша гугла.
4) Для REST есть куча готовых генераторов которые позволят автоматизировать занудное создание ресурсов а для graphql мне пришлось писать такой генератор самому — причем аж 2 раза — первый для сервера а второй для клиента.
Всё пишется просто, комфортно.
И вообще не понял, причём тут graphqlCool к meteor. От слова совсем.
А теперь головные боли:
Добавьте ещё головную боль в виде размазанной бизнес-логики по мутациям / орм с кучей дублируемого кода, для чего пришлось использовать написать генераторы кода.
Как итог можно смело прогнозировать, что через определенное время проект будет быстрее и дешевле переписать, чем развивать. Так что не используйте graphql
А бизнес логика как и положено лежит бизнес слое который собственно и инжектируется через DI в типы мутации и запросы, слава DDD при желании можно переписать все на REST тупо генерированием тех же REST ресурсов для каждого объекта логики.
Ну и не только я, всякие канторки поменшьше, типа Facebook ((((-:
Быстрее и дешевле делать на GraphQL. Так что используйте GraphQL!
У меня Apollo на фронте и на сервере.
В случае если использовать изоморфные приложения как у вас тогда все просто и понятно, но как эксперимент — попробуйте представить, просто представить что backend у вас не ваш любимый node.js а на java или например как в моем случае php.
Попробуйте описать 2 раза типы мутации и запросы ручками для клиента на том-же Apollo и следом за этим описать эти-же типы запросы и мутации на сервере используя например
- php = laravel + folklore/graphql
- java = Spring + graphql-java
я бы посмотрел на это =)
P.S
используйте GraphQL только если у вас изоморфное приложение на платформе node.js
Клиенту фиолетово, что там на бэкенде, лишь бы он отдавал нужные ему данные в правильном формате. Если так сложно и больно делать бэкенд на php или чём-то там ещё, сервер на expresse поднимается несколькими строчками кода и забывается вся эта боль (-:
Посмотрел на graphql-java: не нашёл ничего страшного и сверхъестественного. У себя думаю для некоторых пользователей попробовать поднять GraphQL на Elixir-e.
Так что всё выше Вами описанное — это не проблемы GraphQL.
Разные бэкенды — это не неправильная или странная архитектура, а, ровно наоборот, выбор языка бэкенда лучше подходящего для той или иной задачи.
И дело не в том, что сложно сделать бэкенд на PHP, а в том, что схему графа придётся описывать дважды в случе сервера и клиента на разных языках. В общем случае — сколько языков используется с одной схемой — столько раз придётся описать.
Может я не так понял, но выше предлагали писать одно и тоже на php и на java.
И опять же: это всё — не проблема GraphQL.
И дело не в том, что сложно сделать бэкенд на PHP, а в том, что схему графа придётся описывать дважды в случе сервера и клиента на разных языках.
А зачем на клиенте схему описывать?
GraphQL — новый взгляд на API. Ч.1