Перейти к содержанию

Пагинация

Различные модели пагинации дают различные возможности клиенту

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

Plurals

Самый простой способ показать связь между объектами,- это поле, которое возвращает множественный тип. Например, если мы хотим получить список друзей R2-D2, мы могли бы просто запросить всех из них:

{
  hero {
    name
    friends {
      name
    }
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

Slicing

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

{
  hero {
    name
    friends(first: 2) {
      name
    }
  }
}

Но если мы просто получаем первые две записи, мы можем захотеть переключаться между страницами списка; если клиент получает первые две записи, он может захотеть отправить второй запрос для получения следующих двух записей. Как нам дать эту возможность?

Pagination and Edges

Есть целый ряд способов, которыми мы могли бы сделать нумерацию страниц:

  • Мы могли бы сделать что-то вроде friends(first:2 offset:2), чтобы запросить о двух следующих в списке.
  • Мы могли бы сделать что-то вроде friends(first:2 after:$friendId), чтобы попросить двух следующих после последнего друга, котрого мы извлекаем.
  • Мы могли бы сделать что-то вроде friends(first:2 after:$friendCursor), где мы получаем курсор из последнего пункта и используем его для разбивки.

В итоге, мы обнаружили, что разбивка на основе курсора - самая мощная из всех. Особенно если курсоры непрозрачны, или может быть внедрено смещение разбивки на основе ID, используя разбивку на основе курсора (путем преобразования курсора в интервал или ID). Использование курсоров так же дает дополнительную гибкость, если модель разбивки меняется в будущем. Чтобы напомнить, что курсоры непрозрачны и на их формат нельзя положиться, предлагаем кодировать их в base64.

Это приводит нас к проблеме; подумайте; как мы получаем курсор от объекта? Мы бы не хотели, чтобы курсор жил в User; это свойство соединения (connection), а не объекта. Таким образом, мы могли бы хотели ввести новый слой косвенности; поле friends должно дать нам список разрезов (edges) и каждый разрез содержит курсор и подчиненный узел:

Request

{
  hero {
    name
    friends(first:2) {
      node {
        name
      }
      cursor
    }
  }
}

Понятие грани также оказывается полезным, если есть информация, специфичная для грани, а не для одного из объектов. Например, если мы хотим показать "время дружбы" в API, встраивание его в грань является естественным.

End-of-list, counts, and Connections

Теперь у нас есть возможность постраничной пагинации через соединение с использованием курсоров, но как мы узнаем, что достигли конца соединения? Мы должны продолжать выполнения запросов, пока мы не получим пустой список назад, но мы бы очень хотели чтобы соединение (Connection) сказало нам, что мы дошли до конца, чтобы нам не пришлось выполнять еще один запрос. Точно так же, что, если мы хотим узнать дополнительную информацию о самом соединении; например, каково общее число друзей у R2-D2?

Чтобы решить обе эти проблемы, поле «friends» может вернуть объект подключения. Объект подключения будет иметь поле для граней, а также другую информацию (например, общего количества и информации о том, существует ли следующая страница). Таким образом, наш окончательный запрос может выглядеть как:

{
  hero {
    name
    friends(first: 2) {
      totalCount
      edges {
        node {
          name
        }
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

Обратите внимание, что мы также могли бы включить endCursor и startCursor в объект PageInfo. Таким образом, если нам не нужно какой-либо дополнительной информации о том, что содержит грань, мы можем не запрашивать получение граней, т. к. у нас уже есть необходимые для разбивки курсоры. Это потенциального приводит к улучшению удобства и простоты использования для соединений; вместо того, чтобы просто показать список граней, мы можем также показать специальный только список узлов, чтобы избежать слоя косвенности.

Complete Connection Model

Очевидно, что это является более сложным, чем изначальная задумка получения множества! Но, приняв этот сценарий, мы разблокировали ряд возможностей для клиента:

  • Возможность постраничной навигации по списку.
  • Возможность запросить информацию о самом соединении,таких как totalCount или pageInfo.
  • Возможность запрашивать информацию о самой грани, такой как cursor или friendshipTime.
  • Возможность изменения того, как наш бэкенд делает разбиение на страницы, так как пользователь просто использует непрозрачные курсоры.

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

{
  hero {
    name
    friendsConnection(first: 2, after: "Y3Vyc29yMQ==") {
      totalCount
      edges {
        node {
          name
        }
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friendsConnection": {
        "totalCount": 3,
        "edges": [
          {
            "node": {
              "name": "Han Solo"
            },
            "cursor": "Y3Vyc29yMg=="
          },
          {
            "node": {
              "name": "Leia Organa"
            },
            "cursor": "Y3Vyc29yMw=="
          }
        ],
        "pageInfo": {
          "endCursor": "Y3Vyc29yMw==",
          "hasNextPage": false
        }
      }
    }
  }
}