Comments 132
В нашем мире нет ничего идеального, и GraphQL тому хороший пример. Но черт побери, даже сейчас эта шарманка хорошо ускоряет разработку большинства клиент-серверных приложений. Но опять-таки надо исходить из бизнес задач, кому-то GraphQL может не подойти или не зайти.
С удовольствием жду продолжения!
Выбор, собственно стоял между GraphQL, JSON-API и Protobuf. По внутренним соображениям — победил GraphQL.
Серверную разработку (особенно прототипирование), в сравнении со слепленным на коленке REST, GraphQL не ускоряет от слова «совсем». Скорее даже тормозит. Но на долгой дистанции получается серьезный выигрыш. Вы пишите функционал один раз и получаете:
1. Внутренний программный интерфейс для веб-интерфейса/сайта.
2. Программный интерфейс для проприетарных мобильных/десктопных приложений.
3. Публичный программный интерфейс для сторонних разработчиков.
Кроме того, нам это помогает избежать целого класса ошибок, со вводом пользователя —
Тут выручают кастомные скаляры, со своими валидаторами.
Что касается разработки на фронте — то у нас в большинстве случаев (если не были введены новые типы), фронт генерируется автотматически на основе интроспекции схемы. Новые же типы приходится досконально разбирать и описывать. Но только один раз — потом только баги править.
Вообще, я не гуру фронтенда, и допустил кучу ошибок в дизайне архитектуры на фронте. Но в целом это работает.
Кроме того, нам удалось изящно решить проблему ролей/прав используя graphql. Но это материал для отдельной статьи.
Из технологий:
на фронте — Vue + Apollo + самописный фрейм поверх vue
на бэке — самописный монстрик на php слепленный из webonyx/graphql, railt/sdl, doctrine/orm, cboden/ratchet (для подписок)
бд — postgresql,
кеш — redis,
поиск — elasticsearch
А мне вот наоборот импонирует подход GraphQL именно в серверной реализации. Я имею в виду Query Resolvers и DataLoader. Когда у нас есть единая реляционная база, нам легко вытащить нужные данные. Но если данные приходят из разных источников — написание API-фасада это боль.
А так мы можем объединить данные из реляционки, mongo, redis, да даже другого REST API. Лишь бы он поддерживал операцию getByIds<K, V>(ids: K[]): V[]
. И все это без проблемы N + 1 запросов.
Кроме того, нам удалось изящно решить проблему ролей/прав используя graphql. Но это материал для отдельной статьи.
Это очень и очень интересная, больная тема. Даёшь статью!
1. NON_NULL
До этого не обращал внимание и все ОК было, а теперь тоже глаз режет, вот зачем так? :)
Остальную боль подтверждаю, но плюсов пока больше.
Но вопросы безопасности, как правило, не касаются graphql на прямую. Graphql отвечает лишь за соблюдение структуры данных. Всё остальное ложится напрямую на плечи разработчика. Что-то можно валидировать через кастомные скаляры. Вы могли заметить в статье я использую тип `UInt` — как не сложно догадаться — это `unsigned integer`, это кастомный скаляр со своим валидатором, тоже самое может касаться и других полей. Часто вижу скаляры Email, Url, Phone. Но есть такие вещи, которые можно провалидировать только в рантайме -вроде владения/принадлежности.
В общем, GraphQL не предоставляет каких-то специальны инструментов безопасности.
Если поле отсутсвует, то что нужно сделать? Не обновлять его? Или уставновить его в null? Суть в том, что разрешить значение null и разрешить отсутсвие поля — это разные вещи. Тем не менее в GraphQL — это одно и тоже.
github.com/facebook/graphql/issues/542
Сделать тоже самое для вводимых типов нельзя. Для этого есть ряд предпосылок, но отчасти это связано и с тем, что в качестве формата данных при транспорте используется json. Тем не менее, при выводе, для конкретизации типа используется поле __typename. Почему нельзя было сделать тоже самое при вводе — не очень понятно.
github.com/facebook/graphql/pull/395
В следующем материале, я хочу рассказать о том, как я сражаюсь (и иногда успешно) с некоторыми из описанных выше проблем.
Было бы очень интерестно
Пункт 2. Разделение ввода и вывода
это скорее благо (да здравствует cqs).
На одном старом проекте одни и те же модельки (=классы) использовались и на вход и на выход. Вы не представляете как меня задолбало по коду смотреть какие поля и когда надо заполнять.
Ваше предложение использовать атрибуты для указания где input/output сильно усложнило бы язык.
Строгий и явный контракт лучше, как по мне.
PS: прошу прощения, попал не в ту ветку с телефона
Вы не представляете как меня задолбало по коду смотреть какие поля и когда надо заполнять.
Для этого есть интроспекция схемы.
Ваше предложение использовать атрибуты для указания где input/output сильно усложнило бы язык.
Тут без субъективщины не обойтись. Мне это сильно упростило бы жизнь.
Проблемы с производительностью небыли здесь затронуты потому, что это не проблемы GraphQL как языка. Это проблемы его реализаций. Что бы затронуть эти темы, мне пришлось бы писать материал под заголовком "что не так с apollo/webonyx/railt/graphcool". А это совсем другая история :)
это не проблемы GraphQL как языка. Это проблемы его реализаций
Мне как прикладному разработчику это не так важно в языке проблема или в его реализации. Мне важно, что я или не могу решить эту проблему или для решения мне надо фактически отказаться от GraphQL. Про реализацию это действительно отдельная проблема, видел решения, когда выгребают из БД все данные, а потом отдают только то что запрашивают через graphQL.
При девелопменте используем любые запросы, на бекенде делаем в лоб.
На проде используем только предварительно одобренные запросы, которые уже можно оптимизировать так как запросы не могут быть любыми.
выгребают из БД все данные, а потом отдают только то что запрашивают через graphQL
Вообще, не так это плохо как звучит, как по мне. Получается как rest, но меньше отдавать данных.
Получается как rest, но меньше отдавать данных.
Ну и стоит ли городить еще одну абстракцию только ради экономии трафика? Ну и главное, в rest, если будет упор в производительность, можно сделать либо отдельный ендпоинт, либо добавить параметр и оптимизировать выдачу нужным образом. С GraphQL, конечно, можно сделать также, но зачем он тогда нужен?
Меня в GraphQL пугают две вещи: производительность и безопасность.Мне кажется, такие опасения возникают из-за восприятия GraphQL API как интерфейса к БД, хотя по факту GraphQL не привязан к источнику хранения данных.
Представьте, что у вас есть REST-endpoint, который позволяет получить список каких-либо сущностей. В GraphQL вы можете использовать тот же самый код, который получает эти данные для REST. Только GraphQL позволяет отдавать пользователю только те поля, которые он запросил, таким образом, экономя трафик пользователя и уменьшая количество запросов, которые он должен сделать, чтобы получить необходимые данные.
Например, данные вы можете получить с помощью локального HTTP-запроса к микросервису. Несколько запросов к микросервисам внутри локальной сети вашей компании для формирования GraphQL-ответа отработают намного быстрее, чем несколько запросов от пользователя к REST API.
Например, есть какое-то поле, которое очень затратно выбирать и по хорошему нужно использовать разные запросы на БДСоздайте отдельный resolver для этого поля и укажите, каким образом это поле должно извлекаться из хранилища.
Или нужно разграничить набор возвращаемых данных в зависимости от роли пользователяА каким образом вы бы реализовали это в REST API? В GraphQL, например, разграничить доступ к полям можно с помощью директив, указанных на каждом поле, где необходима проверка доступа. Вот статья, описывающая такой подход: codeburst.io/use-custom-directives-to-protect-your-graphql-apis-a78cbbe17355, но это не единственный возможный вариант решения.
Только GraphQL позволяет отдавать пользователю только те поля, которые он запросил, таким образом, экономя трафик пользователя и уменьшая количество запросов
Это решается написанием нехитрого фасада, где в результате будет ровно один REST запрос под нужные данные, а заодно может еще кэширование с настройкой получения данных от внутренних запросов (circuit breaker) и т.д. И такой вариант позволит еще и уменьшить нагрузку на внутренние сервисы. Поскольку, если внутренний сервис отдает 20 полей, а нам надо 3 из них, то с GraphQL на выходе получим только уменьшение трафика, а нагрузка по получению этих 20 полей и прокачиванию их по внутренней сети останется.
Это решается написанием нехитрого фасада, где в результате будет ровно один REST запрос под нужные данные,
Только не один запрос, а 2^N разных запросов, включая критерии (аргументы) выборки для каждого поля, их отношения с другими сущностями и сопутствующие операции (ну, например, транзакционность нескольких параллельных запросов на 10 разных типов данных).
У меня мозг расплавился от того, как только я начал представлять как это реализовать в RESTful реализации и сколько костылей придётся вставлять.
Чтобы понимать — приведите пример того, как на RESTful выглядел бы запрос на получение пользователя по id и его 10 друзей в онлайне, где его createdAt обязан быть в формате RFC3339, а updatedAt, например, в виде отношения к дате создания.
{
user(id: 42) {
createdAt(format: RFC3339)
accountLife: updatedAt(diff: "createdAt")
friends(status: ONLINE, count: 10) {
...
}
}
}
Только не один запрос, а 2^N разных запросов
Нет, к фасаду будет один запрос. Внутри фасада будут запросы уже в зависимости от того, что требуется получить.
Чтобы понимать — приведите пример того, как на RESTful выглядел бы запрос на получение пользователя по id и его 10 друзей в онлайне
Не совсем понял, что вы имеете в виду без описания того, что где хранится. Но, ок, вот так будет выглядеть, как вариант:
/user-with-online-friends/42?friendsCount=10
Url можно придумать поизящнее, написал что пришло в голову. Если подобных запросов много, то разумно придумать какую-то систему или набор параметров.
Не совсем понял, что вы имеете в виду без описания того что где хранится.
Я по-моему довольно чётко описал:
1) Пользователь с id 42 (это вы сделали)
2) Дата создания пользователя (у вас в примере этого нет)
3) В формате RFC3339 (у вас в примере этого нет)
4) Отношение даты создания к дате последней активности в произвольном формате (у вас в примере этого нет)
5) Список друзей (у вас в примере этого нет)
6) В количестве 10 штук (это есть)
И прошу заметить — это совершенно тривиальный запрос для GraphQL API. т.е. я не выдумаю лишних сложностей.
Сложности начинаются, когда ещё просят статистику активности этого пользователя с группировкой по X, Y, Z, в интервале A...B, с выборкой по какой-нибудь географии и отношению к другому пользователю. Я не то чтобы выдумаю, но похожие задачи у меня на работе были и когда это вертелось бы поверх REST — был лютейший трешак.
Все, что вы написали в пунктах 2,3,4,5… [очень много букв]… то нужно отдельно продумывать как это лучше сделать...
Или можно просто взять GraphQL )
Если таких вариантов больше десятка-двух, то нужно отдельно продумывать как это лучше сделать.
Ну так я написал, что нужно учитывать все возможные ситуации и сочетания в количестве 2^N^M от полей и аргументов соответственно и попросил привести в пример частный запрос на RESTful эндпоинт, который удовлетворяет этому частному случаю, когда какому-то одному клиенту из сотен (или тысяч) потребовался конкретно этот кейс с перечисленными выше условиями.
Не думаю что выдумать такой адрес GET запрос и нужные параметры доставит вам проблем. Всего лишь приведите пример такого адреса и аргументов. Это так сложно?)))
Кажется не очень, т.к. на написание аналогичного запроса GraphQL мне потребовалось пара минут, а вы пытаетесь аргументированно доказать, что «гибкость запросов», которую приводят в пользу GraphQL — не аргумент, и можно тоже самое на RESTful реализовать без проблем.
Не думаю что выдумать такой адрес GET запрос и нужные параметры доставит вам проблем.
Как же вам еще объяснить, что я не буду закладывать параметры для формата данных и т.д? Это должно быть заложено в реализацию. Написать как данные получить из БД и замапить в dto с преобразованием данных в нужный формат, вы этого хотите?
Еще раз, все зависит от контекста. Как правило вариантов хотелок и разных выборок ограниченное количество. И для каждого такого варианта пишется отдельный эндпоинт с осмысленным и коротким названием. Все. Если таких хотелок огромное множество, например как у Facebook, нужен какой-то механизм формирования запросов и тут GraphQL вероятно зайдет хорошо. Но, опять же, таких проектов по моему опыту абсолютное меньшинство, а благодаря хайпу, GraphQL пытаются прикрутить не потому, что он необходим, а потому что могут.
Это должно быть заложено в реализацию. Написать как данные получить из БД и замапить в dto с преобразованием данных в нужный формат, вы этого хотите?
Не думаю что выдумать такой адрес GET запрос и нужные параметры доставит вам проблем.
/user-with-online-friends/42?friendsCount=10
Я думаю, что в данном случае адрес, который отвечает заданным критериям должен выглядеть как-то так:
/user-with-createdAt-using-rfc3339-with-updatedAt-diff-with-createdAt-with-online-friends/42?friendsCount=10
-
Верно?
Простой апи — перечисление полей, которые нужно вернуть:
/users?fields=id,name,..
Сложный апи — используй
POST /users/search.json
{
search: {...}
output_format: {...}
}
, в {...} можно запихнуть конфигурацию любого уровня сложности
Потом такие «а давайте одну точку входа сделаем!» — не вопрос:
POST /graphql
{
users(...search_params...): {
...
}
}
По сути тот же json, просто другой синтаксис — но фейсбук же не идет проторенными путями, верно?
Жирный плюс — они написали спецификацию. Надо её теперь дополнить и стандартизировать.
К примеру, одни фильтруют так:
{
users(id: 12) {
id
}
}
Другие — так:
{
users(id: {eq: 12}) {
id
}
}
Третьи так:
{
users(_eq: [id, 12]) {
id
}
}
Сделать единый формат, который будет поддерживать большинство из возможностей SQL WHERE, сделать инструменты на фронте и на сервере, которые это все генерируют и транслируют (типа hasura), и даже домохозяйка сможет создавать крутые SPA сайты. Мир, дружба, жвачка, и безработные айтишники, которые раньше делали «сайт под ключ за 5к рублей». А еще — генерация крутых админок под любой бекенд, был бы только в наличии дизайн приятный :)
Только эта мысль должна была придти тем, кто хотел доказать, что и на RESTful можно писать сложные штуки. Тогда бы я смог аргументированно высказаться, мол «а зачем это, когда всё и так уже есть», кинув ссылку на спеки GraphQL. Но это уже не суть.
Верно?
Конечно нет. Что ж так тянет все усложнять. Я предлагаю еще упростить:
Чтобы понимать — приведите пример того, как на RESTful выглядел бы запрос на получение пользователя по id и его 10 друзей в онлайне
Вам это же для чего-то же было нужно? Вот и заведите ендпоинт под этот кейс: /yourcase?userId=… — все, там будут выдаваться только нужные поля, только 10 онлайн друзей и с нужным форматом даты. Такой способ позволяет полностью контролировать выдачу и по возможности избавиться от пустой нагрузки. В вашем изначальном требовании не было условия, что таких запросов может быть много с разным набором параметров. Но выше уже предложили решения и для этого случая.
Даже в отдельном проекте отдел фронтэнда — это тоже некие «они» со своим бизнес-планом у которых своё виденье того, как нужно, где и что загружать.
Что не мне нужно, а всем тем сотням или тысячам клиентов, которые будут использовать этот API.
Какие сотни тысяч клиентов? В статье написан пример использования — SPA-админка, у которой есть фронт, есть бэк. Все, нет никаких тысяч клиентов. Если у вас именно такой проект, то да, вам лучше использовать GraphQL, но не надо распространять это автоматом на все проекты в мире.
это тоже некие «они» со своим бизнес-планом у которых своё виденье того, как нужно
Для этого есть процесс интеграции фронта с бэком, бэк не делает сферического коня в вакууме, а так или иначе знает что нужно будет фронту. Взаимодействовать на этапе разработки все равно придется.
На этом предлагаю завершать дискуссию, которая явно зашла в тупик.
Можно что-нибудь такое придумать. В результате все сводится к написанию DTO с нужными геттерами.
// /user/42?fields=id,name,createdAt,accountLife,onlineFriends&limit[onlineFriends]=10
class UserDTO
{
protected $model;
public function __construct(UserModel $model)
{
$this->model = $model;
}
public function getCreatedAt()
{
return formatDate('RFC3339', $this->user->created_at)
}
public function getAccountLife()
{
return formatTimeDiff('RFC3339',
$this->user->updated_at, $this->user->created_at
);
}
/** @return ActiveQuery */
public function getOnlineFriends()
{
return $this->model->getFriends()
->andWhere(['status' => 'online']);
}
public function __get($name)
{
$getter = 'get' . ucfirst($name);
$value = (method_exists($this, $getter)
? $this->$getter()
: $this->user->__get($name));
return $value;
}
}
class UserModel
{
/** @return ActiveQuery */
public function getFriends()
{
return $this->hasMany(User::class, ['second_user_id' => 'id'])
->viaTable('friends', ['id' => 'first_user_id']);
}
}
class Engine
{
...
private function prepareFields()
{
...
foreach ($fields as $field) {
$value = $modelDTO->__get($field);
if ($value instanceof ActiveQuery) {
$query = $value;
if (isset($this->request->limit[$field])) {
$query->limit($this->request->limit[$field]);
}
$value = $query->all();
}
$result[$field] = $value;
}
}
}
Кроме того, более-менее рабочий вариант GQL можно сотворить и просто на более стандартных подходах и форматах, используя совершенно стандартные инструменты, коих полно на всех возможных ЯП. А это, в свою очередь, тоже профит.
Ниже вариант решения вашей задачи без использования специальной спеки типа GQL. Единственная вводная заключается в том, что сервер «обучен» понимать что фильтрация может идти по любому полю таблицы (поля без "_") и при этом имеются специальные «директивы», которые описывают дополнительные действия и начинаются со знака "_" (тоже довольно общепринятый подход). Сами понимаете что научить такому сервер очень просто (для упрощения все на JS):
// client
import qs from 'qs';
...
const usedId = 42;
const query = qs.stringify({
createdAt: {
_format: 'YYYY-MM-DDTHH:mm:ssZ'
},
_join: {
friends: {
status: 'ONLINE',
_limit: 10
},
accountLife: {
updatedAt: {
_diff: 'users.createdAt'
},
_include: ['updatedAt', 'sid']
}
}
});
fetch(`/v1/users/${usedId}?${query}`).then(console.log).catch(console.error);
// server
import qs from 'qs';
...
server.get('/users/:id', (req, res, next) => {
console.log(req.params.id); // id юзера
console.log(req.query); // полностью восстановленный объект запроса
});
В итоге визуально немного похоже на GQL, но с одной большой разницей — тут не используются никакие специализированные решения. Обычные объекты, обычный url-encoded, обычный REST. Да и серверный код для обработки этого также будет довольно примитивен. При этом остается все плюсы REST подхода, а сами запросы очень просто ложаться на SQL.
То, что это инструмент, созданный для решения конкретных задач конкретной компанией. Сама идея просто крутая, но когда его пытаются прикрутить везде, где это возможно, или даже невозможно, становится намного странно.
GraphqQL — это язык обмена данными, а не язык программирования общего назначения.
К примеру, я абсолютно не понимаю, для чего использовать один и тот же тип для описания добавления сущности и обновления. Это разные операции, а желание возникает из-за использования подобного в REST/ORM моделях, лично я с этим не согласен. Это и называется мутации, а не обновление сущности и они могут быть сложными, вложенными и т.д. Обычный CRUD и так автоматизируется легко и непринужденно (в том числе, в GraphQL). Если хотите так все заабстрагировать, делайте это на стороне языка программирования.
GraphqQL — это язык обмена данными, а не язык программирования общего
назначения.
GraphQL — это два языка: язык запросов (QL) и язык определения схемы (SDL). В материале я сделал упор на SDL, так как я активно его использую. Вы можете написать свою имплементацию для декларации схемы/интроспекции. В любом случае какую бы вы не написали реализацию — вам всё равно придется мапить это имеющуюся систему типов.
К примеру, я абсолютно не понимаю, для чего использовать один и тот же тип для описания добавления сущности и обновления.
А это и не должен быть один тип. Проблема описанная в этом примере вообще никак не касается разделения таких типов. И с разными типами будет точно такая же проблема.
Читайте материал.
Это и называется мутации, а не обновление сущности и они могут быть сложными, вложенными и т.д
Я ожидал именно таких комментариев. И специально для этого сделал в начале материала ремарку, а конце материала написал: Graphql — это не совсем (а бывает и совсем не) про CRUD.
Но видимо это так не работает. Читайте материал.
Обычный CRUD и так автоматизируется легко и непринужденно (в том числе, в GraphQL).
Нет. Читайте материал.
Если хотите так все заабстрагировать, делайте это на стороне языка программирования.
Про генерацию кода я тоже сказал. Читайте материал.
Мне сейчас прям даже обидно. Я же писал для кого?
1. Null
1.1 Вывернутые наизнанку монады.
1.2 Неоднозначность nullable при вводе.
2 Неудобное разделение ввода и вывода.
3 Отсутствие полиморфизма при вводе.
4 Отсутствие дженериков.
5 Отсутствие неймспейсов.
Прочитайте наконец материал, чтобы не приходилось в комментариях спрашивать автора о чем материал. Ну это уже просто неуважение какое-то (
Нет никакой неоднозначности при вводе, просто не используйте PostInput и в create и в update, вы на это не ответили ничего (уважайте комментаторов, пожалуйста).
Расскажите, в каком языке описания данных есть полиморфизм и дженерики, неймспейсы? А что дальше, лямбда-функции, циклы, условия? Зачем вообще тогда выдумывать и использовать декларативные языки и форматы, если можно просто взять любой готовый язык общего назначения и написать для него, к примеру, еще один компилятор?
Почему все это становится недостатками GraphQL — абсолютно непонятно. Замените GraphQL на любое другое слово — смысл статьи не изменится. «Что не так с JSON»? Там нет дженериков. Что не так с HTML? Там нет полиморфизма.
Про null и восклицательные знаки — абсолютная вкусовщина
Я так и сказал.
Нет никакой неоднозначности при вводе, просто не используйте PostInput и в create и в update
input PostCreateInput {
text: String
}
input PostUpdateInput {
text: String
}
Куда делась неоднозначность? Всё верно — никуда.
Расскажите, в каком языке описания данных есть полиморфизм и дженерики, неймспейсы.
Я не могу вспомнить язык с полиморфизм и дженериками, который не подходил бы для описания данных. Другое дело, что они еще и исполнятся могут, но это совсем другая история.
А что дальше, лямбда-функции, циклы, условия?
А вот это уже рантайм. Но вообще-то без привязки лямбд, к полям типов у вас и приложение работать не будет...
Зачем вообще тогда выдумывать и использовать декларативные языки и форматы, если можно просто взять любой готовый язык общего назначения и написать для него, к примеру, еще один компилятор?
А вот это мысль кстати. Взять только декларативную часть от какого-то языка, выкинуть весь рантайм — возможно получится недурной язык описания схемы данных.
Почему все это становится недостатками GraphQL — абсолютно непонятно.
Я уж как мог так объяснил. Не знаю как сделать, чтобы вам было понятно.
Что не так с JSON
Я в этой статье написал, что не так с JSON. Читайте уже материал )
Что не так с HTML? Там нет полиморфизма.
В HTML c полиморфизмом вообще-то всё ок )
А вот это мысль кстати. Взять только декларативную часть от какого-то языка, выкинуть весь рантайм — возможно получится недурной язык описания схемы данных.
Так я и не понимаю, почему вы хотите этого от GraphQL? Возьмите JS, если вам хочется свободы, многие базы данных так и сделали.
Стандарты и протоколы хороши не тем, что в них напихано все на свете, а тем, что используя их можно сделать, что угодно дешево. Вот GraphQL позиционируется, как низкоуровневый формат, а сверху вы можете реализовать что угодно — дженерики, обязательные поля и т.д.
В HTML c полиморфизмом вообще-то всё ок )
Ну да, он вообще тьюринг-полный, кстати, почему бы вам не использовать его?
PostUpdateInput
А тут нет никакой неоднозначности, если у вас 2 разных способа обновить сущность, то нужно делать 2 разных Input. Еще раз повторю — это мутации, а не обновления сущности Post, не нужно к ним так относиться! Это описание действия в бизнес-логике, а не универсальный CRUD-метод!
Я просто процитирую сам себя:
Думаю, что стоит сделать небольшую ремарку относительно того, где я применяю данный язык. Это довольно сложная SPA-админка, большая часть операций в которой — это довольно нетривиальный CRUD(сложновложенные сущности). Значительная часть аргументации в данном материале связана именно с характером приложения и характером обрабатываемых данных. В приложениях другого типа (или с другим характером данных) таких проблем может и не возникнуть в принципе.
Вы не можете выкинуть CRUD из статьи о CRUD.
Умоляю: сжальтесь надо мной и прочитайте материал.
Вы не можете выкинуть CRUD из статьи о CRUD.
Пожалуйста, сжальтесь над читателями и прочитайте свой заголовок, а затем список выдуманных претензий. Где в заголовке слово CRUD? Какое отношение дженерики, неймспейсы, null имеют непосредственно к CRUD, а не к чему угодно в программировании?
Вся статья заключается в том, что вы взяли инструмент, который вам не нужен, потом заявили, что в нем нет того, что нужно (неожиданно), а затем создали кликбейт-заголовок «Что не так в инструменте X» и не написали ни слова, что не так в инструменте X.
Простите, пожалуйста.
Человек спрашивал, о планах по статье об организации ролей/прав.
Очень хочу написать, если руки дойдут.
По факту, GraphQL — не всем подойдет, и может оказаться совсем не тем инструментом, который вам нужен.
Дайте технологию про которую нельзя такое сказать. И даже на вашем примере эта мысль не вполне раскрыта. В вашем случае (сложные CRUD'ы) и т.п. — какой же инструмент подошел бы вам лучше в итоге?
Суть в том, что разрешить значение null и разрешить отсутствие поля — это разные вещи.
Это действительно разные вещи, но вы их совершенно неверно трактуете:
- Отсутствие поля = оно не определено в структуре/классе.
- Если же поле определено, то оно "всегда есть", и в случае nullable может иметь значение NULL.
Отступление от этого принципа ведет в тупик (требует разных NULL'ей, приводит к семантической путанице/неоднозначности). Это регулярно демонстрирует JSON (и другие "текстовые" языки описания данных).
В этом ключе, пример с update не корректен, точнее говоря налицо проблема в дизайне API.
Предложите тогда свой вариант, как обновить только одно поле объекта, не трогая остальные его поля. Я не пользую GraphQL (ещё), но тоже всегда просто опускал поля, которые не нужно обновлять, в этом самом JSON без какой-либо путаницы
JS (и JSON как следствие), на мой взгляд, являются языками с массой недочетов/нелогичностей ради "упрощения и удобства", которые ведут к массе проблем. В свою очередь GraphQL идет следом по тем же граблям, с добавлением пары новых.
Если совсем кратко (и вынужденно высокопарно), то основная претензия в том, что предлагается замещение содержания формой представления информации, т.е. форма становится определяющей по отношению к содержанию.
Теперь мы наблюдаем, что попытки описать различные смыслы и "содержание запросов" этими языками, во многих случаях местами приводят к неоднозначности и прочей боли.
- доктор, я порезался.
- не ешьте с ножа.
- доктор, как правильно есть с ножа.
- не ешьте с ножа.
- доктор, как мне лучше есть с этого ножа.
- не ешьте с ножа.
— Ешьте только весь обед целиком.
На мой вопрос-то так и не ответили.
На мой вопрос-то так и не ответили.
Ответ более-менее очевиден.
Семантически нам нужно указать какие поля мы хотим обновить, а какие не трогать. В случае безсхемного JSON достаточно логично просто поместить в update-запрос только обновляемые поля с новыми значениями. Но возникают проблемы, если язык описания подразумевает наличия схемы (описание структур), из которого следует умолчание о null-значения у "отсутствующих" полей.
Выход достаточно стандартный = передавать в аргументах update-запроса не экземпляр структуры, а массив структур key-value. [ { name: "field1", vallue: null }, {name: "field2", vallue: "not a null"}, ...]. Конечно, так теряется контроль схемы на уровне языка, но это следствие недостатков языка (формы описания). Тем не менее, так недостатки формы (языка) не перерастают в проблемы API.
В таком случае, под проблему в дизайне API можно вообще подвести любой тезис из статьи )
Немного оффтоп, но мне это напоминает, ситуацию, которая сложилась у меня с поддрежкой Razer.
Была у меня клава с макросами. И решил я запилить макрос, который раз в час жмякает пару кнопок. Есть там такая модель макроса: нажал один раз — включил макрос, нажал второй — выключил. Но вот жеж косяк — если ты запустил макрос, то хотябы один раз он должен отработать. Нельзя отключить макрос до завершения текущей итерации и даже профиль переключить нельзя, пока макрос не завершен. И я, как программист, понимаю в чем проблема. Но мне, как пользователю, нужен функционал. Я написал в поддержку (ну, думаю, может в следующем патче на по поправят косяк). Описал суть проблемы с такими макросами, они очень долго не могли понять, что именно мне не нравится (тут, возможно, виноват мой не очень высокий скилл в заморском наречии). Потом они наконец сообразили в чем косяк, и сказали мне: "Не пишите такие макросы". А потом закрыли тикет. Заплатки на это дело нет по сей день.
Так вот к чему это я… всегда можно сказать "не делай так". Вот только проблема от этого не исчезнет )
Проблема дизайна API тут в том, что семантика (смысл) запроса выражается с привязкой/зависимостью к неоднозначностям/недостаткам формы (языка описания запроса).
Это не отменяет недостатков JSON и/или GraphQL, т.е. действительно есть недостатки, в том числе в выразительности и удобства для тех или иных случаев. Но вдвойне неверно допускать протечки этих недостатков в API.
В мутациях отправляешь те поля, которые нужно изменить. Если не нужно изменять значение поля — не отправляешь его. Если отправляешь в качестве значения null, то и в бд запишется null, если валидация позволяет. Вы хотите, чтобы можно было отправлять поля со значениями null, а они при этом не меняли своего значения?
В реляционных базах null — это отдельный вид значения, это не 0 и не пустая строка. Вот и GraphQL это отражает. Поэтому для какой-нибудь схемы {id: String!} соответствует значение {id: ""}, и не соответствует {id: null}.
Допустим, как вы и сказали, я хочу отправлять только часть полей. При этом, если они переданы, то они не должны быть null. Как описать эту ситуацию в системе типов GraphqQL?
Примерно также как это делается в GraphQL — перечислением полей, которые надо получить.
1.2. INPUT
А вот при вводе, nullable — это вообще отдельная история. Это косяк уровня checkbox в HTML
Во-первых, перестаньте мыслить понятиями REST, когда работаете с GraphQL. Это другой подход, и мутации в GraphQL это не PATCH-методы из REST, а, скорее, аналог обычных функций в языках программирования. Когда, например, вы в любой ORM в метод «save» передаете объект как аргумент, вы же не можете каким-то образом «не передать» часть атрибутов этого объекта — вы либо передаете значения полей, которые изначально получили из БД, либо ставите null, и в БД тоже значение после сохранения становится null.
Во-вторых, если у вас есть кейсы, где для одной и той же модели нужно обновлять разные группы полей, никто не мешает создать для этого разные мутации. Маловероятно, что у вас 50 различных комбинаций обновляемых полей — чаще всего это 2-3 кейса на модель. Даже если смотреть с точки зрения обычных языков программирования — лучше создать несколько методов, каждый под свои нужды, чем один супер-метод, который делает все на свете.
4. Дженерики
А что не так в GraphQL c дженериками? А всё просто — их нет.
Зачем это нужно? Никто не пишет схему руками — для большинства языков программирования есть DSL для GraphQL с дженериками на уровне этого языка и всеми сопутствующими плюшками.
Когда, например, вы в любой ORM в метод «save» передаете объект как аргумент, вы же не можете каким-то образом «не передать» часть атрибутов этого объекта
Могу, если перегрузить функцию.
вы либо передаете значения полей, которые изначально получили из БД, либо ставите null
Именно так. Только не "либо ставите" null, а "либо устанавливается дефолтное значение".
Например вот так:
input ExampleInput {
value: Int = 0
}
И, что бы это поведение работало парвильно, нужно расценивать вот это выражение:
input ExampleInput {
value: Int
}
как
input ExampleInput {
value: Int = null
}
Но тогда косяк в том, что концепция partial input просто перестает работать.
Другими словами, я не считаю понятия undefined
и null
тождественными.
Если кому-то и так норм — я рад за них, мне — не норм.
Именно так. Только не «либо ставите» null, а «либо устанавливается дефолтное значение».
Да, естественно, но это не меняет сути. Я думаю, вас также не устроило бы, если бы все атрибуты, которые вы не передали, сбрасывались бы на дефолтные значения.
Но тогда косяк в том, что концепция partial input просто перестает работать.
Другими словами, я не считаю понятия undefined и null тождественными.
Это не косяк, это ваша привычка мыслить подходами REST. Рассматривайте мутации GraphQL как просто функции в любом языке программирования, а input-типы как объекты в ООП (хорошо, без возможности иметь дефолтные значения атрибутов), и все станет восприниматься гораздо понятнее.
Рассматривайте мутации GraphQL как просто функции в любом языке программирования, а input-типы как объекты в ООП
Ok, у нас есть обьект с 10 полями, как быть если мы хотим обновить только 5 из нихЁ остальным не меняя значение. Создавать по фуникции для каждой комбинации?
Скажем, есть такой тип:
type User {
login: String!
role: String!
name: String
email: String
age: Int
}
Предположим, что в нашем приложении есть две формы: 1. Форма обновления «credentials» 2. Форма обновления «профиля». В первой форме нужно обновлять только поля «login», «role», а во второй поля «name», «email», «age».
Есть два варианта решения этой задачи:
Вариант 1, не очень удачный на мой взгляд — делаем input тип со всеми полями и одну мутацию:
input UserInput {
login: String!
role: String!
name: String
email: String
age: Int
}
type Mutation {
updateUser(input: UserInput!): User
}
Перед отображением форм достаем юзера с сервера через api. В первой форме поля «login» и «role» подставляем в
UserInput
из формы, остальные поля из того, что пришло из api. Во второй форме поля «name», «email», «age» подставляем в UserInput
из формы, остальные поля из того, что пришло из api. Вызываем updateUser
с заполненным UserInput
.Вариант 2, более адекватный — делаем свой input тип для каждого кейса и две мутации:
input UserCredentialsInput {
login: String!
role: String!
}
input UserProfileInput {
name: String
email: String
age: Int
}
type Mutation {
updateUserCredentials(input: UserCredentialsInput!): User
updateUserProfile(input: UserProfileInput!): User
}
В каждой форме используем соответствующий input и мутацию.
Может, это и делается немного проще с точки зрения отправки на сервер из фронтенда, но путем создания монструозных типов на сервере, где какой-нибудь
Boolean
может иметь не 2 значения (true/false
), а 3 (true/false/undefined
), что на мой взгляд еще хуже.Вызвать какую мутацию? У вас их две. А с ростом количества полей, их количество также будет расти. Подождите немного, я подробно разберу эту тему, в одном из следующих материалов (я пока не решил какая из тем пойдет вперед — народ сильно топит за "разграничение доступа").
Вызвать какую мутацию? У вас их две.
Посмотрите первый вариант в моем комментарии выше. Я специально написал его для любителей выполнять одним методом все на свете. Второй вариант с двумя мутациями — это для приложений с четкими различающимися кейсами изменения модели. Например, отдельная форма изменения email, отдельная форма обновления имени и фамилии и т.п. Очевидно, что там даже на бэкенде логика разная будет выполняться в зависимости от формы, и делать это все одним методом с кучей if'ов, мягко говоря, не айс.
Также непонятно, если у вас CRUD, и на каждую сущность форма со всеми полями, зачем отправлять только часть из них, если у вас на руках полная модель.
Оу, простите, виноват. Я почему-то подумал, что это про второй вариант.
Также непонятно, если у вас CRUD, и на каждую сущность форма со всеми полями, зачем отправлять только часть из них, если у вас на руках полная модель.
Чтобы реализовать концепцию partial update
— отправлять только то, что было изменено. Прошу вас, потерпите немного. Иначе мне придется выложить все свои соображения по этому поводу в комментариях, а потом написать пост единственным содержимым которого будут ссылки на эти комментарии )
partial update
реализуется только усложнением входных типов. А делать partial update
ради partial update
, либо бороться против 2Кб дополнительного трафика (или использовать это как основное средство против race conditions) выглядит не очень хорошей мотивацией)partial update
— хорошее средство защиты от перезаписи при одновременном редактировании несколькими пользователями? :) Если Вася и Игорь одновременно загрузили страничку с формой обновления сущности, Вася поменял описание и цену, а Игорь поменял название и исправил опечатку в описании, то partial update
никак не защитит от того, что Игорь перезапишет все изменения Васи в поле описания сущности. В таких случаях применяется что-то более нормальное — например, optimistic lock.optimistic lockИ как вы его примените в этой ситуации, позвольте спросить.
Как именно вы в таком сценарии оптимистичную блокировку внедрите? Сам принцип какой будет? Синхронизировать данные вебсокетами, как в гугл доксе? Открывая форму редактирования, отправлять серверу дополнительный запрос вида «сейчас я начну редачить вот эту сущность»?
Upd А, кажется, понял. Отправлять не только новые значения, но и старые — чтобы сервер сравнивал, совпадают ли эти старые значения, перед обновлением. Мороки это, конечно, добавит знатно ;)
Optimistic lock это просто и поддерживается некоторыми ORM вообще из коробки. Надеяться на
partial update
как на основное решение при совместном редактировании считаю непрофессиональным. Где-то оно, может, и будет помогать худо-бедно, а где-то, где, скажем, поле «описание» у товара редактируется в 10 раз чаще других полей — не будет.medium.com/paypal-engineering/batch-an-api-to-bundle-multiple-paypal-rest-operations-6af6006e002
apigee.com/about/blog/technology/restful-api-design-can-your-api-give-developers-just-information-they-need
Собственно, GraphQL — это удобный контракт между бэкендом и фронтендаом, а для SPA с компонентным подходом — фрагменты, лежащие рядом с UI-компонентами сильно упрощают поддержку приложения.
Как бы весь дизайн GraphQL идет от нужд фронтенда в первую очередь, поэтому чистые бэкендеры все его плюсы и не видят.
Что не так с GraphQL