Зачем нужны ребра в соединении Relay / GraphQL?

В конфигурации схемы Relay / GraphQL отношения «один-ко-многим» (с разбивкой на страницы) указываются как в учебный пример

type ShipConnection {
  edges: [ShipEdge]
  pageInfo: PageInfo!
}
type ShipEdge {
  cursor: String!
  node: Ship
}

Однако соединение "один-к-одному", выполненное ShipEdge, кажется избыточным. Почему мы не можем переместить курсор на ShipConnection и сохранить массив Ship ID в виде ребер?

type ShipConnection {
  edges: [Ship]
  pageInfo: PageInfo!
  cursor: String!
}

Какие проектные решения требовали одного дополнительного объекта на каждые edge в отношении «один ко многим»?


person Michael Ho Chum    schedule 24.10.2015    source источник


Ответы (3)


Поле edges предоставляет вам место для ввода данных для каждого края. Например, вы можете захотеть поместить туда поле creator или priority, описывающее, кто добавил преимущество и насколько важны отношения, соответственно.

Если вам не нужна такая гибкость (или другие функции, которые вы получаете с подключениями, например разбиение на страницы), вы можете использовать простой тип GraphQLList. См. этот ответ Подробнее о разнице между подключениями и списками.

person wincent    schedule 25.10.2015

(Обновлено с дополнительными пояснениями)

Есть 3 способа представить массив данных в GraphQL:

  1. Список: используйте, когда у вас есть конечный список связанных объектов, которые вы можете получить сразу все. В GraphQL SDL это представлено как [Ship].
  2. Узлы: используйте, когда вам нужно разбить список на страницы, обычно потому, что в нем могут быть тысячи элементов. Обратите внимание, что это не является частью спецификации Relay и, как таковая, не поддерживается клиентом Relay (вместо этого вы должны обернуть элемент краем, как описано в # 3), но некоторые другие клиенты, такие как Apollo, более гибкие и поддержите эту конструкцию (но вам нужно предоставить больше шаблонов). В GraphQL это будет представлено как type ShipConnection { nodes: [Ship], pageInfo: PageInfo! }.
  3. Ребра: используйте, когда помимо разбивки на страницы вам также необходимо предоставить дополнительную информацию для каждого ребра в соединении (подробнее читайте ниже). В GraphQL вы бы написали это как type ShipConnection { edges: [ShipEdge], pageInfo: PageInfo! }.

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

type Query {
  ships: [Ship]       // #1
  shipsConnection: [ShipConnection]
}

type ShipConnection {
  nodes: [Ship]       // #2
  edges: [ShipEdge]   // #3
  pageInfo: PageInfo!
}

type PageInfo {
  endCursor           // page-based pagination
  hasNextPage
}

type ShipEdge {
  cursor: String!     // edge-based pagination
  node: Ship
  // ... edge attributes
}

type Ship {
  // ... ship attributes
}

Списки (№1) следует использовать только тогда, когда вы знаете, что количество элементов не будет расти (например, если у вас есть Post, вы можете захотеть вернуть tags в виде списка, но вы не должны делать этого с comments). Чтобы выбрать между №2 и №3, есть две причины использовать ребра вместо простых узлов:

  • Это место для атрибутов, специфичных для края. Например, если у вас есть User, который принадлежит многим Group, в реляционной базе данных у вас будет таблица UserGroup с user_id и group_id. Эта таблица может иметь дополнительные атрибуты, такие как role, joined_at и т. Д. GroupUserEdge тогда будет местом, где вы можете получить доступ к этим атрибутам.

  • Есть место для курсора. Relay, помимо разбивки на страницы (с использованием pageInfo), поддерживает разбиение на страницы на основе краев. Зачем Relay нужен курсор для каждого ребра? Поскольку Relay интеллектуально объединяет требования к данным из всего вашего приложения, у него может уже быть соединение с теми же параметрами, которые вы запрашиваете, но в нем недостаточно записей. Чтобы получить недостающие данные, он может запросить данные в соединении после курсора некоторого края.

    Я понимаю, что это может сбивать с толку, учитывая, что в базах данных тоже есть курсоры, а на запрос есть только один курсор. Ретрансляционное соединение на самом деле не является запросом, это скорее набор параметров, которые идентифицируют запрос. Курсор края соединения - это набор параметров, которые определяют позицию в соединении. Это более высокий уровень абстракции, чем чистый курсор запроса (помните, что ребра должны иметь возможность идентифицировать позицию даже через соединение, которое может не быть запросом БД или быть скрытым сторонней системой). Из-за такой необходимой гибкости одного курсора для соединения было бы недостаточно.

person Petr Bela    schedule 10.12.2015
comment
Да, в сценарии с кораблем вы можете захотеть createdAt и color на самом корабле; Я просто привел их как абстрактные примеры имен полей. Обратите внимание, что в некоторых доменах у вас может быть несколько ребер, указывающих на один и тот же узел, и вы, возможно, захотите узнать, когда каждое ребро (в смысле графа) было добавлено, и поэтому будете использовать createdAt. Я использовал color как общее имя свойства, но вы можете подумать и о других вещах, которые могли бы описать природу края. например, weight (насколько важен край) или creator (кто установил ссылку) и т. д. Я отредактирую свой ответ, чтобы избежать этой путаницы. - person wincent; 26.01.2016
comment
Это полезный ответ, но я все еще не могу себе представить, когда реле нужно будет извлекать данные с помощью курсора из середины соединения. В ситуации, когда у вас есть соединение с теми же параметрами, которые вы запрашиваете, но в нем недостаточно записей, будет достаточно курсора для последнего ребра. - person SamBarnes; 13.05.2017
comment
Пример из моей головы: вы получаете список комментариев, но затем последний комментарий удаляется. Итак, чтобы получить следующий пакет комментариев, вам нужно начать с последнего курсора. Я уверен, что есть еще много вариантов использования. Дело в том, что Relay старается быть как можно более универсальным и достаточно надежным, чтобы управлять всем, что происходит с данными. - person Petr Bela; 14.05.2017
comment
@PetrBela Когда вы выполняете пагинацию набора клавиш, удаленная запись не влияет на вас. Я не понимаю, зачем вам нужен предыдущий курсор комментариев для перехода на следующую страницу. - person Massimo Fazzolari; 19.03.2021
comment
@MassimoFazzolari Да, похоже, мой предыдущий пример был не лучшим. Однако смысл наличия курсора (как для смещения, так и для разбивки на страницы) остается прежним. Вопрос в том, почему Relay требует, чтобы у каждого узла был курсор, когда все соединение уже имеет курсор? Возможно, чтобы учесть использование одного и того же соединения в двух разных компонентах, один с разбивкой по страницам на 3, а другой на 10 элементов? Возможно, это крайний случай, но Relay все равно сможет с этим справиться. (Это, вероятно, используется в комментариях Facebook, которые сначала отображаются как топ-3, но затем вы расширяете его, он добавляет 8 или около того и т. Д.) - person Petr Bela; 19.03.2021
comment
@PetrBela Насколько я понимаю, вы получаете небольшую оптимизацию, когда у вас есть компонент, запрашивающий 3 элемента, но вы уже загрузили первые 10. Но ваш пример с Facebook не выиграет от этого, потому что вы не хотите загружать 10 элементов. в кеше, если в большинстве случаев вам нужно показать только 3. Я до сих пор не вижу ни одного реального варианта использования, в котором ребра были бы полезны. - person Massimo Fazzolari; 20.03.2021
comment
@MassimoFazzolari. Если вы видите 3 комментария в ленте новостей, а затем переходите на страницу с подробностями, на которой указано 10, Relay может просто получить 7 новых. Я, наверное, не могу объяснить больше, потому что я не был автором, но я так понимаю это. FB имеет множество конкретных вариантов использования и оптимизаций, которые, вероятно, не стоят сложностей в большинстве проектов, и это, похоже, один из них. (Вы можете заметить, что Apollo вообще не занимается этим, поскольку они не считали это достаточно распространенным.) - person Petr Bela; 21.03.2021
comment
@MassimoFazzolari Edges по-прежнему полезны, поскольку вы можете прикреплять данные, специфичные для ребер, что я, надеюсь, объяснил в своем ответе. С другой стороны, пограничные курсоры имеют более теоретическое значение, которое может не быть строго необходимым в большинстве проектов, но, поскольку клиент Relay работает таким образом, они по-прежнему требуются. - person Petr Bela; 21.03.2021
comment
@PetrBela Не могли бы вы указать на какой-нибудь реальный API, который прикрепляет данные, специфичные для ребер? Shopify и Github этого не используют. Также прикрепление данных к ребрам означает, что вам нужно иметь разные типы подключения для каждой модели, что, на мой взгляд, делает ваш код менее пригодным для повторного использования. Края и курсоры для каждого узла кажутся мне классическим примером чрезмерной инженерии. - person Massimo Fazzolari; 24.03.2021
comment
@MassimoFazzolari У меня нет списка тех, кто использует пограничные данные в своих API. Я просто пытался объяснить, для чего их можно использовать и почему Relay нужна такая структура. И да, на практике в 95% случаев ребра вам не понадобятся, но авторы Relay решили покрыть теоретические 5%, написав спецификацию, которая их покрывает (и, скорее всего, эти 5% используются в Кодовая база FB). На этом я бы закончил обсуждение, так как я не являюсь автором спецификации и у меня нет дополнительной информации, кроме той, о которой я только что предположил. - person Petr Bela; 25.03.2021
comment
В последней заметке я бы добавил, что я действительно использовал данные Edge в одном из своих API. Однако я обнаружил, что на практике легче преобразовать таблицы отношений в автономные объекты, так как с ними удобнее работать. Другими словами, вместо orgs - ›org_users -› таблицы пользователей, где тип Org имеет пользовательское соединение с org_user, являющимся краем, лучше иметь orgs - ›members -› таблицы пользователей, где у типа Org есть элементы соединение, и каждый Member имеет связанный User. - person Petr Bela; 25.03.2021

Мы написали в блоге статью о различиях между простой схемой GraphQL и схемой, специфичной для Relay:

https://www.prisma.io/blog/connections-edges-nodes-in-relay-758d358aa4c7

person schickling    schedule 02.06.2016