TL;DR
Реализация динамических типов на GraphQL выглядит довольно естественно. Однако, есть определенные сложности.
Intro
Бывает так, что пользователи хотят поэкспериментировать с данными в приложении, чтобы посмотреть, как удобнее реализовать изменившиеся требования бизнеса. Например, добавить 100 новых типов данных, атрибуты и связи между ними. Динамические типы данных очень хорошо способствуют этому, не требуя постоянных усилий разработчиков по добавлению новых или изменению существующих типов.
Сегодня я расскажу об одном из вариантов реализации динамических типов данных с помощью GraphQL, и о том, с какими сложностями мы при этом столкнулись.
Что такое динамические типы?
Динамические типы — это такая фича приложения, когда пользователи могут определять свои собственные типы данных, а затем создавать объекты этих типов.
Допустим, мы разрабатываем энциклопедию Властелина Колец. Изначально мы предоставили пользователям небольшой набор рас: эльфы, гномы, хоббиты и люди. Также, мы создали несколько героев: Леголас (эльф), Гимли (гном), Фродо (хоббит) и Арагорн (человек).
Таким образом, расы — это типы данных, а герои — объекты этих типов.
Затем администратор логинится в систему и говорит: «Эй, а где Гендальф?» И он сначала добавляет расу Майар, а потом создает героя Гендальф.
Но как это можно реализовать?
Динамические типы с GraphQL
На одном из проектов в Okko мы делаем это с помощью GraphQL. Если в двух словах, то gateway предоставляет API для создания типов и объектов. После того, как создан новый тип, схема на gateway сервисе обновляется, и новый тип становится доступен для запросов и мутаций.
А теперь посмотрим детально, как это работает.
Есть три основных сервиса на бэкенде: gateway, settings и data‑access‑layer.
Gateway — это API gateway, который предоставляет GraphQL API всех бэкенд сервисов при помощи мержа их схем. И он просто перенаправляет API запросы.
Settings сервис отвечает за управление типами и атрибутами типов.
Data‑access‑layer — это главная абстракция над базой данных. Он предоставляет GraphQL API с CRUD операциями над объектами.
Итак, процесс начинается, когда фронтенд отправляет запрос на gateway на создание нового типа (1). Запрос проксируется в settings сервис, который создает новый тип в базе данных (2).
Затем settings отправляет «refresh schema» сообщение в RabbitMQ (3), которое обрабатывается data‑access‑layer сервисом.
Data‑access‑layer в свою очередь отправляет запрос в settings (4) для получения всех типов (5) и атрибутов этих типов (6) из базы данных. Как только data‑access‑layer сервис получает результат (7), он делает две вещи:
обновляет свою собственную GraphQL схему, чтобы появились CRUD операции для нового типа;
отправляет «refresh schema» сообщение в RabbitMQ (8).
Когда gateway получает «refresh schema» сообщение, он фетчит обновленные схемы со всех сервисов (9), мержит их и предоставляет GraphQL API, которое затем может быть использовано для создания объектов нового типа (10).
Почему так сложно? ????
Окей, я признаю, что выглядит все не очень просто. И при виде этой схемы в голове возникает много вопросов. Попробую ответить хотя бы на несколько.
Фетч обновленной схемы и инкрементальный мерж
На шаге (3) settings отправляет сообщение в data‑access‑layer, который в свою очередь вызывает settings (4), чтобы получить GraphQL схему со всеми типами и их атрибутами (7). Так почему бы просто не отправить инкрементальный апдейт (новый тип) в сообщении? Тогда нам вообще не нужны шаги (4)‑(7).
Когда data‑access‑layer получает «refresh schema» сообщение, он должен обновить свою GraphQL схему — добавить CRUD операции для нового типа. И этот инкрементальный мерж схемы — нетривиальная задача. Поэтому пока что мы просто достаем все типы (4)‑(7) и собираем из них новую GraphQL схему.
По той же причине на шаге (9) gateway фетчит схему со всех сервисов, несмотря на то, что схема изменилась только в data‑access‑layer сервисе.
Так что инкрементальный мерж — это наш технический долг и одно из мест, где мы можем сделать систему лучше.
Масштабирование
Теперь давайте посмотрим, что произойдет с бэкенд‑сервисами, когда они будут горизонтально масштабироваться.
Если у нас есть несколько gateway сервисов, тогда каждый из них должен получать «refresh schema» сообщение (8), чтобы обновить свою схему. И это именно то, что происходит, так что тут все хорошо.
Если у нас есть несколько data‑access‑layer сервисов, тогда нам надо быть уверенными, что
каждый инстанс получает «refresh schema» сообщение (3);
когда gateway отправляет «get schema» запрос (9), схема уже обновлена на том data‑access‑layer инстансе, который получил этот запрос.
Очевидно, первое выглядит аналогично предыдущему случаю с шагом (8), но как решается второе?
Элементарно, мой дорогой Ватсон! Мы просто не масштабируем data‑access‑layer сервис. Он всегда запускается как один инстанс, представляя собой единую точку отказа в системе.
Несмотря на то, что это одна из наиболее критичных вещей, которые нам надо исправить, в какой‑то момент команда решила, что лучшее — враг хорошего. И мне кажется, это было верное решение, учитывая сроки, в которые нам надо было уложиться с запуском первой версии системы в прод.
Заключение
Мы рассмотрели один из вариантов реализации динамических типов — с помощью GraphQL, — и выглядит он довольно естественно. Однако в реализации могут возникнуть такие сложности, как мерж схемы, масштабирование сервисов и обработка сообщений.
dimstream
Спасибо за статью. Какие инструменты для работы с GraphQL используете?
pfilaretov42 Автор
Для генерации схемы и эндпоинтов - DGS Framework + самописный код по работе со схемами.
Для тестирования запросов - Insomnia.