Является ли циклическая ссылка схемы graphql анти-шаблоном?

Схема graphql выглядит так:

type User {
  id: ID!
  location: Location
}

type Location {
  id: ID!
  user: User
}

Теперь клиент отправляет запрос graphql. Теоретически User и Location могут бесконечно ссылаться друг на друга.

Я думаю, это антипаттерн. Насколько мне известно, нет промежуточного программного обеспечения или способа ограничить глубину вложенности запроса как в сообществе graphql, так и в сообществе apollo.

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

Итак, если схема graphql допускает циклическую ссылку, должно быть какое-то промежуточное программное обеспечение или способы ограничения глубины вложенности запроса. Или добавьте некоторые ограничения для запроса.

Может, не допустить циркулярную ссылку - это получше?

Я предпочитаю отправлять другой запрос и выполнять несколько операций в одном запросе. Все намного проще.

Обновлять

Нашел вот эту библиотеку: https://github.com/slicknode/graphql-query-complexity. Если graphql не ограничивает круговую ссылку. Эта библиотека может защитить ваше приложение от истощения ресурсов и DoS-атак.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Что такое Apollo Client и зачем он нужен?
Что такое Apollo Client и зачем он нужен?
Apollo Client - это полнофункциональный клиент GraphQL для JavaScript-приложений, который упрощает получение, управление и обновление данных в...
14
0
6 068
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Образец, который вы показываете, довольно естественен для «графика», и я не думаю, что он особенно обескураживает в GraphQL. GitHub GraphQL API - это то, на что я часто смотрю, когда мне интересно, «как люди создают более крупные API-интерфейсы GraphQL», и там обычно есть циклы объектов: Репозиторий имеет Владелец репозитория, который может быть Пользователь, у которого есть список repositories.

По крайней мере, graphql-ruby имеет элемент управления для ограничения глубины вложенности. Очевидно, что Apollo не имеет этого элемента управления, но вы можете создать собственный источник данных или использовать библиотеку DataLoader, чтобы избежать повторного извлечения объектов, которые у вас уже есть.

dataloader предназначен для проблемы запроса N+1. Думаю, это другой вопрос. Лично мне не нравятся круговые ссылки.
slideshowp2 20.12.2018 12:46

Что касается экосистемы узлов, есть graphql-depth-limit :) Он предоставляет правило проверки, которое вы можете добавить прямо в свою схему, которое предотвращает выборку за пределами указанной глубины запроса.

Daniel Rearden 20.12.2018 16:05
Ответ принят как подходящий

По-разному.

It's useful to remember that the same solution can be a good pattern in some contexts and an antipattern in others. The value of a solution depends on the context that you use it. — Martin Fowler

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

С другой стороны, циклические ссылки обеспечивают дополнительный уровень гибкости. Работаем с вашим примером, если мы примем следующую схему:

type Query {
  user(id: ID): User
  location(id: ID): Location
}

type User {
  id: ID!
  location: Location
}

type Location {
  id: ID!
  user: User
}

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

{
  # query 1
  user(id: ID) {
    id
    location {
      id
    }
  }

  # query 2
  location(id: ID) {
    id
    user {
      id
    }
  }
}

Если основными потребителями вашего API являются одна или несколько клиентских групп, работающих над одним и тем же проектом, это может не иметь большого значения. Вашему интерфейсу нужны данные определенной формы, и вы можете разработать свою схему в соответствии с этими потребностями. Если клиент всегда выбирает пользователя, может таким образом получить местоположение и не нуждается в информации о местоположении вне этого контекста, может иметь смысл иметь только запрос user и опустить поле user из типа Location. Даже если вам нужен запрос location, все равно может не иметь смысла раскрывать в нем поле user, в зависимости от потребностей вашего клиента.

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

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

Как и в случае с большинством архитектурных решений, есть компромисс, и правильное решение для вас может быть не таким, как для другой команды.

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

Как указывает @DavidMaze, помимо ограничения глубины клиентских запросов, вы также можете использовать dataloader для снижения затрат на многократное извлечение одной и той же записи из уровня данных. Хотя dataloader обычно используется для пакетных запросов, чтобы обойти «проблему n + 1», которая возникает из-за ленивой загрузки ассоциаций, он также может здесь помочь. Помимо пакетной обработки, загрузчик данных также кэширует загруженные записи. Это означает, что последующие загрузки для той же записи (внутри одного и того же запроса) не попадают в базу данных, а вместо этого извлекаются из памяти.

TL; DR; Циклические ссылки являются анти-шаблоном для API GraphQL без ограничения скорости. API с ограничением скорости могут безопасно использовать их.

Длинный ответ: Да, настоящие круговые ссылки - это анти-шаблон для меньших / более простых API ... но когда вы дойдете до точки ограничения скорости вашего API, вы можете использовать это ограничение, чтобы «убить двух зайцев одним выстрелом».

Прекрасный пример этого был дан в одном из других ответов: Github GraphQL API позволяет вам запрашивать репозиторий, с его владельцем, с их репозиториями, с их владельцами ... бесконечно ... или так вы могли подумать по схеме.

Если вы посмотрите на API (https://developer.github.com/v4/object/user/), вы увидите, что их структура не является круговой: есть промежуточные типы. Например, User не ссылается на Repository, он ссылается на RepositoryConnection. Теперь у RepositoryConnectionделает есть RepositoryEdge, у которого делает есть свойство nodes типа [Repository] ...

... но если вы посмотрите на выполнение API: https://developer.github.com/v4/guides/resource-limitations/, вы увидите, что преобразователи, стоящие за типами, ограничены по скорости (т. е. не более X узлов на запрос). Это защищает как потребителей, которые запрашивают слишком много (проблемы с широтой), так и от потребителей и, которые запрашивают бесконечно (проблемы с глубиной).

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

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

Таким образом, у GraphQL API есть два варианта:

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

Если вы не хотите ограничивать скорость, подход GraphQL к использованию разных типов все же может дать вам ключ к решению.

Допустим, у вас есть пользователи и репозитории: вам нужны два типа для обоих: User и UserLink (или UserEdge, UserConnection, UserSummary ... выбирайте сами), а также Repository и RepositoryLink.

Каждый раз, когда кто-то запрашивает пользователя с помощью корневого запроса, вы возвращаете тип User. Но у этого типа пользователя нет будет:

repositories: [Repository]

в нем было бы:

repositories: [RepositoryLink]

RepositoryLink будет иметь те же «плоские» поля, что и Репозиторий, но ни одно из его потенциально круглых объектных полей. Вместо owner: User был бы owner: ID.

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

Как указывает @ daniel-Rearden, следствием циклических ссылок является то, что они позволяют нескольким документам запроса получать одни и те же данные. По моему опыту, это плохая практика, потому что она делает кеширование запросов GraphQL на стороне клиента менее предсказуемым и более сложным, поскольку разработчик должен явно указывать, что документы возвращают одни и те же данные в другой структуре.

Кроме того, при модульном тестировании трудно генерировать фиктивные данные для объектов, поля / свойства которых содержат циклические ссылки на родительский объект. (по крайней мере, в JS / TS; если есть языки, которые поддерживают это легко из коробки, я бы хотел услышать это в комментарии)

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

В сторону: честно говоря, если бы не практические последствия циклических ссылок, я бы с удовольствием их использовал. Было бы красиво и удивительно представить структуры данных в виде «математически совершенного» ориентированного графа.

Кэширование объектов graphql на стороне клиента по своей сути сложно для всего, кроме корневого элемента запроса, независимо от циклических ссылок.

George Aristy 23.12.2021 03:35

Другие вопросы по теме