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

Курсорная пагинация курсов

Список Академии пока возвращает все курсы сразу. Для большого каталога нужен ограниченный результат и явная граница продолжения. Добавим поле courseConnection с курсором. Оно будет возвращать edges и pageInfo, а старое courses оставим доступным для совместимости учебных документов. Курсор не является номером страницы: он описывает позицию в выбранном устойчивом порядке, после которой клиент хочет продолжить чтение.

В нашем наборе используются ID c01, c02 и c03 одинакового формата. Сортируем их лексикографически и читаем значения больше последнего ID. Это небольшой предметный договор, который нельзя молча переносить на произвольные строковые ключи. Например, c2 и c10 имеют иной лексикографический порядок. Для настоящей базы потребуется согласованная сортировка и индекс, отражающие публичную границу продолжения.

Connection и edge

Каждый edge содержит node с Course и cursor для положения этой карточки. PageInfo сообщает hasNextPage и endCursor. Контейнер позволяет расширить связь метаданными без изменения формы самого Course. Курсор относится к конкретному порядку списка, а не к объекту независимо от всех запросов. Один курс в поисковой выдаче и в списке новинок может иметь разные границы продолжения при одинаковом предметном ID.

Аргумент first ограничен диапазоном от одного до двадцати. По умолчанию берём две карточки. Сервер получает на одну больше, чтобы определить наличие продолжения, но лишнюю не включаем в edges. При этом не вычисляется totalCount. Клиент знает, можно ли запросить ещё, но не знает точное количество оставшихся страниц. Это честный договор ответа, который не создаёт лишнего обещания полного подсчёта.

query Page($after:String){
 courseConnection(first:2,after:$after){
  edges{cursor node{id title}}
  pageInfo{hasNextPage endCursor}
 }
}

Это полный документ чтения страницы. Variables содержит after:null для начала или строку endCursor предыдущего результата. Значение first в примере фиксировано двумя. Клиент не должен сам увеличивать курсор арифметикой или считать его номером страницы. Он возвращает непрозрачное значение, которое получил от сервера, сохраняя возможность изменить внутренний формат в будущем без изменения пользовательского действия.

Формат и проверка курсора

Наш курсор кодирует строку вида course:c01 в base64url. Это кодирование, а не шифрование и не подпись. Человек способен прочитать и изменить его. Поэтому после декодирования сервер проверяет формат и каноническое повторное кодирование. Произвольная строка не должна молча стать пустой границей. Также тип ID внутри курсора должен соответствовать правилам именно этой connection, а не любому полю схемы.

Подделанный допустимый курсор позволяет перепрыгнуть часть публичного списка. Для текущего чтения это приемлемо: курсор не выдаёт право и не раскрывает закрытые записи. Если connection содержит ограниченные данные, правила доступа должны участвовать в самом отборе, независимо от курсора. Непрозрачный формат не является механизмом авторизации. При необходимости можно связать курсор с фильтром, субъектом или подписью, но эти требования следует объяснить отдельно.

Сначала выбираем курсы с ID больше границы, затем сортируем, затем ограничиваем first+1. Такая последовательность важна: ограничение до сортировки может выбрать другой набор, а фильтрация после LIMIT даст неполную страницу. В памяти это несколько читаемых операций массива. В SQL тот же смысл выражается WHERE, ORDER BY и LIMIT с индексом. Курс не выполняет SQL и не выдаёт массив за измеренный запрос базы.

Конец списка и пустая страница

Первая страница текущего набора должна содержать c01 и c02, hasNextPage:true и курсор c02. Следующая — c03 и hasNextPage:false. EndCursor равен курсору последнего выданного edge. Если edges пусты, endCursor равен null. Курсор не придумывается для отсутствующей строки. Клиент может оставить кнопку продолжения выключенной, не пытаясь бесконечно запрашивать то же условие в надежде получить скрытые карточки.

Новое поле выбирает node, поэтому отсутствие метаданных автора третьего курса по-прежнему даёт частичный ответ, если документ просит author. Пагинация не устраняет ошибки вложенных полей и не должна удалять целый edge только ради чистого errors. Иначе количество узлов и граница страницы станут зависеть от доступности метаданных. Наш договор сохраняет карточку и её курсор, а nullable author описывает самостоятельную потерю сведений.

Изменяющийся набор

Курсорная модель устойчивее OFFSET при вставках перед границей, но не создаёт неизменяемый снимок каталога. Если новый курс получает ID меньше последнего, клиент текущего прохода может его не увидеть. Если карточка удалена, следующая выдача уже отражает актуальный набор. Для фиксированного исторического списка понадобится версия снимка или временная граница. Не стоит называть курсор магической гарантией полноты независимо от изменений данных.

Также правило ID>last не требует, чтобы сам last ещё существовал в списке. Продолжение определяется положением ключа в порядке. В нашем коде проверяется формат, а не наличие карточки курсора. Это позволяет продолжить после удаления узла с таким ID. Если продукт требует запрета устаревших курсоров, нужна дополнительная версия или срок. Выбор зависит от того, важнее удобный текущий проход или строгое восстановление конкретного состояния.

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

Самостоятельный вариант — передать first:0, слишком большое first или испорченный after. Ожидается предметная ошибка BAD_USER_INPUT, а не случайное чтение первой страницы. Во время подготовки эти обращения не выполнялись; полное состояние lesson-08 содержит схему и функции. Следующая глава уменьшит повторное получение авторов через запросный загрузчик, сохранив соответствие ID и значение nullable связи.

EndCursor описывает последний выданный элемент, а hasNextPage — наличие дополнительного элемента при текущем чтении. Не нужно определять продолжение только по числу edges: полная страница из двух карточек может оказаться последней. Именно чтение first+1 различает эти случаи без общего COUNT. Клиент сохраняет отдельный признак вместо собственного предположения и поэтому не делает лишний запрос лишь ради обнаружения конца. Это небольшой пример того, как ясный ответ сервера уменьшает количество недоказанных решений в браузере.

Исходники урока

Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-08. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.

Первичные источники: документация 1.