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

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

Спасибо. Интересный материал. Меня больше смущают такие вот моменты с graphql — это отсутствие возможности кастомизировать ответы при ошибке и отсутствие встроенных средств по разграничению доступа. Я так понимаю что проще всего с ошибками это сделать в ответе к поле error. Но это как то сразу все усложняет хотя и так уже все усложнено
Для разграничения доступа никто не мешает использовать те же самые директивы… @hasRole(...)
Про ошибки отлично расписано вот тут
Там же можно найти и ссылку на видео выступления по теме
Теперь давайте все-таки отложим в сторону REST с его различиями null\undefined и попробуем посмотреть на мутации как на функции в языках программирования. И, вместо вашего оригинального технического решения, которое меняет состояние БД в query-запросах (причем, каждое поле в отдельном запросе к БД), можно решить задачу, например, так:
# Создаем input, где все поля - опциональные

input ExampleOptionalInput {
  foo: String
  bar: String
}

# Создаем мутацию с доп. аргументом

type Mutation {
  updateExampleOptionally(input: ExampleOptionalInput!, onlyFields: [String!]): Example
}

причем, каждое поле в отдельном запросе к БД

Для этого умные дядьки давным давно придумали unit of work — запрос к бд будет один. Я очень ответственно подхожу к вопросу лишних запросов.


Я думал о предложенном вами варианте. Но это порождает запросы с неопределенной структурой — в вашем варианте в onlyFields можно передавать что угодно. Ну или как минимум, завести какой-то Enum с перечислением всех полей доступных в данной сущности.


 onlyFields: [ExampleInputField!]

В общем, этот вариант будет работать, но он мне субъективно не нравится.
Потому что у нас получается два источника истины: один — это список полей самой сущности, второй — это Enum перечисляющий поля.

Для этого умные дядьки давным давно придумали unit of work — запрос к бд будет один.

Будет одна транзакция. Запросов UPDATE будет несколько.

Ну или как минимум, завести какой-то Enum с перечислением всех полей доступных в данной сущности.

Естественно, можно много как организовать валидацию. Основной смысл не меняется.

… но он мне субъективно не нравится.

С таким аргументом и не поспоришь :)
Будет одна транзакция. Запросов UPDATE будет несколько.

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


С таким аргументом и не поспоришь :)

Это было мнение. Аргументация была в следующем предложении

В общем, ладно, я чувствую, что все советы не будут тут восприниматься, потому что вы заранее сконцентрировались на идее «сделать аналог метода PATH в REST», и только эта идея кажется вам логичной и красивой. Если вам нравится менять состояние БД через queries — на здоровье :)

Там просто у greabock, насколько я могу припомнить, админка вся строится автоматически на уровне интроспекции сервера, по-этому всё так и сложно. Т.е. нужные унифицированные методы, которые позволяют автоматически строить интерфейс.


По-этому, подозреваю, и потребовалось уйти от красивой доменной модели и реализовать RESTful-подобную CRUD API.

Именно так
админка вся строится автоматически на уровне интроспекции сервера
Можно об этом по подробнее?
А там собственно таже проблема, которую я описал здесь и в прошлой статье. Она допускает значение null при апдейте. Это конечно можно проверить ручками внутри резолвера, но я искал решения, управлять этим поведением на уровне GraphQL.
Думаю, что объяснять суть макросов никому не нужно...

А лучше бы объяснил. Я вообще даже представить не могу как оно там может решить проблему кривого полиморфизма в gql.


Лично у себя в проекте я добавил директиву на поля:


type Paginator {
    of: [Any!]!
}

type Example {
    users: Paginator @generic(field: "of" type: "User")
}

Но работает оно через одно место, т.к. внедряется внутрь цикла компилятора, подменяя структуры, а значит всякие проверки и вывод типов может тут убиться об стену неконсистентными правилами LSP при имплементации интерфейсов.

Шаблон макроса — просто handlepars-like строка или адрес шаблона в дот-нотации.
А дальше, просто подставляем аргументы в шаблон. Полученные строки подмешиваются в исходник, и схема пересобирается еще раз. Так себе решение, но работает.
О, прикольная идея. Можно даже придумать на эту тему что-нибудь, вроде событий пре/постпроцессинга с инъекциями зависимостей. Это будет проще и понятнее большинству, чем вариант с патчем грамматики, который я предлагал.

Отличный материал, спасибо! Мои пробы с GraphQL меня озадачили несколько другими вещами. Было бы интересно услышать мнение практика.


Сразу упомяну стек, на котором пробовал: nodejs + mongodb


Проблема N+1. DataLoader, конечно, её решает почти полностью, особенно с кэшированием. Но в целом у меня сложилось впечатление, что GraphQL хорошо подойдет для нормализованной реляционной базы, подменяя собой join-ы (и уменьшая стоимость абстракций с помощью DataLoader). Но вот если в проекте, например, mongodb и документы набиты подколлекциями, возникает необходимость ограничивать выборку по полям. И, если, где-то в API всё-таки надо получить к ним доступ, то нужно делать другой тип в Query, резолвер которого уже не ограничен по полям при выборе из базы (например, не использует проекцию в mongo).


Почему не заложена возможность получить набор запрошенных полей в самом резолвере? Есть способы вынимания их из аргумента info, но те что я видел, не позволяют просто понять, какие поля относятся к какой ветке и какому уровню. Или я проглядел?


Я обратил внимание на GraphQL по нескольким причинам:


  • нафиг эти статусы, методы и кучу эндпоинтов
  • возможность собрать несколько запросов в один на фронте
  • схема и автодокументирование
  • возможность завернуть в другой транспорт вместо http

Но вот реализация на бэке мне кажется не столько гибкой, как хотелось бы, если я хочу снизить стоимость абстракций.


Как вариант, городить свою версию на JSON-RPC...

Подсмотреть поля в запросе не предусмотрено но это можно сделать при помощи библиотеки graphql-list-fields Как это можно использовать для решения проблемы n+1 я сделал пример в посте habr.com/post/412847
Да-да, я в курсе про DataLoader, в том числе, кажется, и по вашему материалу. Но DataLoader заточен на выборку по id и кэширование его так же. А если мы начнём делать выборки из БД с разными наборами полей, то кэширование нужно выключать. Да и прокинуть доп.переменные (в том числе и graphql-ный info объект, который нужен graphql-list-fields) непросто, потому что DataLoader не даёт передавать документы в .load/.loadMany. И это логично, но неудобно.

В сложных случаях весьма неудобно разбирать поля в graphql-list-fields.
Существует ещё другой класс решений когда строится мост из graphql прямо в базу данных. То есть в этом случае graphql становится remotesql. У меня эти решения пока не вызывают сильного интереса и.к. и не очень безопасно это как мне кажется и нет гибкости то же по моему мнению со стороны.https://github.com/graphile/postgraphile
Это уже чересчур :) Но в целом, попробовав, я решил остаться на JSON-RPC. Бесконечная иерархия запросов, которую даёт GraphQL, мне не требуется, а валидацию можно сделать на JSON Schema и даже, кажется, более серьёзную, со всеми этими min, max, multipleOf, JSON Pointers. Более того, серверную валидацию на схемах можно утащить на фронт, независимо от того, на чём сделан бэк.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории