Инструкция: как сделать фильтрацию и пагинацию на GraphQL и Go
Привет! Меня зовут Артем и я занимаюсь архитектурой решений в Redmadrobot. По долгу службы мне приходится разбираться с разными технологиями и подходами. В этой инструкции я хотел бы показать, как реализовать фильтрацию и два вида пагинации, если у вас Go и вы планируете использовать GraphQL.
Мы уже довольно давно применяем Go для написания backend-сервисов, но обычно все ограничивается старым добрым REST-ом или gRPC. На одном из новых проектов было принято решение использовать GraphQL для клиентского API.
Эту технологию часто ругают за потенциально бесконечную глубину запросов, за проблему «N+1 запросов», отсутствие механизма пагинации «из коробки» и много за что еще. Для решения большинства этих проблем мы выбрали библиотеку gqlgen, которая умеет справляться с «N+1», бесконечной глубиной , и всем остальным. Кроме фильтрации и пагинации. О них и пойдет речь ниже.
Быстрое погружение в тему
В веб-дизайне и дизайне программного обеспечения под пагинацией понимают постраничный вывод информации, то есть показ ограниченной части информации на одной (веб)-странице (например, 10 результатов поиска или 20 форумных тредов). Она повсеместно используется в веб-приложениях для разбиения большого массива данных на странице и включает в себя навигационный блок для перехода на другие страницы — пагинатор.
Различают два вида пагинации:
- Нумерованные страницы;
- Последовательные страницы.
Есть еще подход с бесконечным скроллом, но это скорее UI-трюк, нежели какой-то отдельный вид пагинации.
Реализация нумерованных страниц
Этот подход вводит такие понятия, как limit и offset, которые хорошо известны любому знающему SQL человеку. Фактически, это самый простой и понятный способ сделать пагинацию. Для начала нужно создать необходимые модели в файле schema.graphqls:
Потом дополнить структуру Resolver в файле resolver.go строкой todos []*model.Todo, сгенерировать резолверы командой gqlgen generate и убедиться, что сгенерировался файл с резолверами schema.resolvers.go с пустым резолвером для списка Todo.
Теперь можно написать реализацию пагинации. Для простоты в примере использован обычный массив с несколькими моделями Todo.
Можно запустить сервер и убедиться, что он стал отзываться на запросы вида:
Подход хорош своей простотой, но на этом его преимущества заканчиваются и начинаются проблемы. Их корень в динамической природе современного веба, а нумерованные страницы заточены на статический контент. В результате этой несостыковки появляются пропуски и дублирования элементов. Для решения этих проблем и была придумана пагинация с помощью последовательных страниц, она же Cursor Based Pagination.
Реализация последовательных страниц
Пагинация с применением курсоров несколько сложнее, т.к. вводит ряд дополнительных понятий, характерных только для GraphQL.
Connection — поле, данные из которого нужно пагинировать, содержит в себе edge-ы;
- Edge — мета-информация об объекте списка. Cодержит в себе поля node и cursor;
- Node — объект списка, он же модель данных;
- PageInfo — параметры пагинации.
Более подробно об этих понятиях можно почитать здесь.
Для реализации этого подхода потребуется чуть больше кода и правок конфигов. В первую очередь необходимо убедиться, что файл gqlgen.yml содержит параметр autobind, смотрящий на каталог с моделями:
После этого необходимо создать go-модели для обеспечения механизмов пагинации. Для этого в каталоге с моделями можно создать файл model.go с таким содержанием:
А потом добавить соответствующие модели в schema.graphqls:
Теперь нужно дополнить структуру Resolver в файле resolver.go строкой users []*model.User, сгенерировать резолверы и убедиться, что они создались корректно:
Осталось добавить реализацию для UsersConnection и Edges. Здесь также для простоты реализации используется обычный массив с пользователями:
Можно запустить сервер и убедиться, что он реагирует на запросы с пагинацией через курсор:
В ответе должен быть json похожий на этот:
Реализация фильтрации
Фильтрация — довольно простая концепция, поэтому ее я оставил напоследок. Покажу на примере с моделью Todo, которую мы уже реализовали выше. Нужно добавить новый input-параметр в файл с моделями GraphQL и указать этот параметр в запросе:
В зависимости от задачи тут может быть сколько угодно полей, по которым можно фильтровать, но не стоит выходить за пределы разумного. Далее нужно выполнить повторную генерацию моделей и написать реализацию:
Теперь можно проверить работоспособность фильтрации:
Заключение
Как видно, ничего сложно в реализации механизмов пагинации и фильтрации на GraphQL нет. При этом все еще хотелось бы иметь эти механизмы доступными «из коробки», а не писать каждый раз руками.
Тестовый проект, в котором уже реализованы все эти концепции можно найти здесь.