В системах управления контентом (или CMS) часто приходится работать с огромными и постоянно меняющимися массивами данных. Так что оптимизация производительности уже не роскошь, а необходимость.
Привет! Я Олег, Python-разработчик в Kokoc Group, и сегодня расскажу, как ускорить работу с данными в CMS Wagtail и сделать разработку проще и приятнее с помощью GraphQL и Graphene. В статье разберу реальные примеры и покажу процесс настройки конкретной системы.
Теория. Когда и почему пора переходить на GraphQL: реальный пример с динамическими данными
В начале пути, когда у вас несколько пользователей и минимальная нагрузка на сервер, разница между REST API и GraphQL кажется незначительной. И REST API все еще работает, как добротный старый велосипед. Но вот вы набираете скорость: растет число пользователей и количество запросов, REST API начинает скрипеть под нагрузкой, и GraphQL становится настоящим спасением.
Особенно для современных приложений, которым необходима гибкость в управлении данными, будь то интернет-магазины, системы бронирования или Telegram-боты.
Покажу на примере, бота, которого я делал для одного из проектов
Бот делался для спортивных онлайн-марафонов. Он давал пользователям всю необходимую информацию по похудению, планы тренировок (при наличии подписки), возможность смотреть рецепты, сохранять свой прогресс: фото до и после, вес, анализы — и рассчитывать КБЖУ, причем внутри бота за счет интеграции с онлайн-анализатором.
Технически бот позволял пользователям настраивать меню и выбирать, какие данные они хотят видеть. Например, одному достаточно названия рецепта, а другому нужен полный комплект: фото, описание, калории и видеоурок.
Если бы я использовал REST API, управление этими запросами могло бы стать сложной задачей. Пришлось бы справляться с множеством эндпоинтов и сложной логикой, несмотря на возможность указать параметры для получения только нужных полей. И как следствие, усложнять архитектуру и увеличивать нагрузку на сервер.
Представьте, что у нас есть 1000 пользователей, которые настраивают меню и запрашивают рецепты. Если мы используем REST API, каждый запрос возвращает фиксированный набор данных, скажем, в 100 КБ, из которых пользователю нужно только 10 КБ. Это значит, что 90% данных передаются в никуда, и сервер, по сути, работает вхолостую.
GraphQL же позволяет выбирать только действительно нужные данные, даже если они сложны и вложены. Хотите получить только название рецепта? Легко. Нужен полный комплект данных с фото, описанием и видео? Проще говоря, это — «шведский стол», на котором выбираешь только нужное.
Конечно, не каждый проект нужно срочно переводить на GraphQL. Если ваше приложение работает стабильно, запросов немного и данные не слишком динамичные, REST API вполне справится. Но если сервис растет, запросы становятся сложнее, и хочется оптимизировать работу с данными — самое время пересесть с «велосипеда» REST на GraphQL, чтобы быстро и легко «маневрировать» в мире динамически меняющихся данных.
Практика. Создание приложения и работа с GraphQL
Я обещал показать весь процесс настройки системы. Вот что мы сделаем:
-
Создадим простое приложение на Wagtail, где будем работать с персонажами, локациями и эпизодами. Это поможет увидеть, как GraphQL упрощает работу с данными.
-
Настроим GraphQL в Django-проекте: установим необходимые инструменты, чтобы GraphQL мог работать с нашим приложением.
-
Напишем запросы и мутации. Я покажу, как создавать запросы для получения данных и мутации для их изменения, чтобы управлять данными было легко.
-
Научимся использовать интерфейс GraphQL для тестирования и отладки GraphQL-запросов. Это поможет проверить, что все работает правильно.
В результате вы получите практический опыт работы с GraphQL и сможете перенести его на свои проекты.
Разбираемся с настройкой
Для начала определимся со структурой моделей в нашем приложении
Если у вас еще нет приложения на Wagtail, то вот подробная инструкция по его созданию.
Определим модели в подпапке models в нашем проекте:
# modesl/location.py class LocationTags(TaggedItemBase): content_object = ParentalKey( to='Location', on_delete=models.CASCADE, related_name='tagged_items' ) class Location(ClusterableModel): name = models.CharField(verbose_name=_("Location name"), unique=True) type = models.CharField(verbose_name=_("Location type")) dimension = models.CharField(verbose_name=_("Location dimension")) tags = TaggableManager(through='LocationTags', blank=True, related_name='location_tags') created = models.DateTimeField(auto_created=True, auto_now=True) modified = models.DateTimeField(auto_now=True)
# modesl/episode.py class EpisodeTags(TaggedItemBase): content_object = ParentalKey( to='Episode', on_delete=models.CASCADE, related_name='tagged_items' ) class Episode(ClusterableModel): name = models.CharField(verbose_name=_("Episode name")) air_date = models.DateField(verbose_name=_("Episode air date")) code = models.CharField(verbose_name=_("Episode code"), unique=True) tags = TaggableManager(through='EpisodeTags', blank=True, related_name='episode_tags') created = models.DateTimeField(auto_created=True, auto_now=True) modified = models.DateTimeField(auto_now=True)
# models/character.py class CharacterTags(TaggedItemBase): content_object = ParentalKey( to='Character', on_delete=models.CASCADE, related_name='tagged_items' ) class EpisodesStreamBlock(StreamBlock): episode = SnippetChooserBlock("project.Episode") class Character(ClusterableModel): name = models.CharField(verbose_name=_("Character name")) status = models.CharField(verbose_name=_("Character status")) species = models.CharField(verbose_name=_("Character species")) gender = models.CharField(verbose_name=_("Character gender")) image = models.ForeignKey( "wagtailimages.Image", on_delete=models.CASCADE, verbose_name=_("Character image"), null=True ) location = models.ForeignKey( "project.Location", on_delete=models.SET_NULL, verbose_name=_("Character current location"), null=True ) episodes = StreamField( EpisodesStreamBlock(), use_json_field=True, verbose_name=_("Episodes in which the character appeared") ) tags = TaggableManager(through='CharacterTags', blank=True, related_name='character_tags') created = models.DateTimeField(auto_created=True, auto_now=True) modified = models.DateTimeField(auto_now=True)
# wagtail_hooks.py @register_snippet class LocationSnippetViewSet(SnippetViewSet): model = Location menu_label = _('Locations') add_to_settings_menu = False add_to_admin_menu = True exclude_from_explorer = False @register_snippet class EpisodeSnippetViewSet(SnippetViewSet): model = Episode menu_label = _('Episodes') add_to_settings_menu = False add_to_admin_menu = True exclude_from_explorer = False @register_snippet class CharacterSnippetViewSet(SnippetViewSet): model = Character menu_label = _('Characters') add_to_settings_menu = False add_to_admin_menu = True exclude_from_explorer = False
Мы определили модели для нашего приложения и настроили их для работы в админке Wagtail. В следующем разделе перейдем к интеграции Wagtail с GraphQL через Graphene, чтобы обеспечить гибкое взаимодействие с данными и оптимизировать запросы. А также рассмотрим, как настроить GraphQL-схему и запросы для наших моделей, чтобы максимально эффективно использовать возможности GraphQL.
Устанавливаем пакеты
Установка graphene-django
Для начала нужно установить пакет graphene-django, который позволяет интегрировать GraphQL в Django-проект. Это можно сделать с помощью pip:
pip install graphene-django
Пакет содержит все необходимые инструменты для работы с GraphQL в рамках Django.
Определяем модуль API и добавляем его в URLs
Далее нужно создать модуль для вашего API, который будет обрабатывать GraphQL-запросы, и подключить его к маршрутам (urls.py).
Структура модуля:
В urls.py модуля необходимо определить два основных эндпоинта:
# api/urls.py # Определяем пространство имен для этого набора маршрутов app_name = "api" # Определяем список URL-путей для работы с GraphQL urlpatterns = [ # Путь для API GraphQL без интерфейса GraphiQL (используется для выполнения запросов) path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=False))), # Путь для интерфейса GraphiQL, который позволяет интерактивно тестировать запросы path('graphiql/', csrf_exempt(GraphQLView.as_view(graphiql=True, pretty=True))), ]
csrf_exempt в примере необходим для отключения проверки CSRF-токена. В реальных проектах следует учитывать меры безопасности, такие как аутентификация и защита от CSRF-атак.
В urls.py проекта подключаем наш модуль:
# core/urls.py urlpatterns = [ ... path('api/', include(api_urls, namespace='api')), ... ]
Настраиваем схемы для Graphene
Схема GraphQL — это основной элемент, который описывает структуру данных и запросов. Чтобы ее создать, нужно определить файл schema.py в модуле API. В этом файле создаются классы для обработки запросов (Query) и мутаций (Mutation).
# api/schema.py # Определяем класс мутаций, который объединяет все мутации для персонажей, эпизодов и локаций class Mutation(graphene.ObjectType, CharacterMutation, EpisodeMutation, LocationMutation): class Meta: # Описание для основной мутации description = "Main Mutation" # Определяем класс запросов, который объединяет все запросы для персонажей, эпизодов и локаций class Query(graphene.ObjectType, CharacterQuery, EpisodeQuery, LocationQuery): class Meta: # Описание для основного запроса description = "Main Query" # Создаем схему GraphQL, которая включает в себя как запросы, так и мутации schema = graphene.Schema(query=Query, mutation=Mutation)
Добавляем Graphene-Django в настройки проекта
Теперь нужно интегрировать Graphene-Django в проект, добавив его в список установленных приложений и указав путь к созданной схеме в файле settings.py:
INSTALLED_APPS = [ ... "graphene_django", ... ] ... GRAPHENE = { 'SCHEMA': 'project.api.schema.schema', }
Эта конфигурация позволит Django находить и использовать вашу GraphQL-схему.
Определяем Nodes, Queries и Mutations
Nodes:
В GraphQL, Node(Type) — тип данных, который моделирует определенную сущность или объект в системе. Вот основные аспекты, которые помогут вам понять, что такое Node и как он работает:
-
Типизация и моделирование. Node — это GraphQL-тип, который определяет, какие поля и типы данных доступны для запросов. Например, если у вас есть CharacterNode, это значит, что он представляет данные о персонаже и описывает, какие поля доступны для запроса, такие как имя, статус и изображение.
-
Связь с Django. Node связывает модель Django с GraphQL-схемой. Он преобразует данные модели Django в формат, понятный GraphQL. Это позволяет GraphQL-запросам обращаться к данным Django-моделей без необходимости вручную обрабатывать их.
-
Разрешители (Resolvers). Каждый Node может иметь методы, называемые разрешителями, которые определяют, как получать данные для каждого поля. Эти методы отвечают за выполнение запросов и возврат нужных данных. Например, resolve_episodes в CharacterNode возвращает список эпизодов, связанных с персонажем.
-
Связи и Поля. Node может включать различные поля, которые определяют связи с другими типами данных. Например, поле episodes в CharacterNode может указывать на связанные эпизоды, а location — на местоположение персонажа. Это помогает строить сложные запросы и получать связанные данные.
Определим наши «Ноды»:
# api/nodes/base.py # Определяем интерфейс TaggableInterface для объектов, которые могут иметь теги class TaggableInterface(graphene.Interface): # Поле 'tags', которое представляет список тегов в виде строки tags = graphene.List(graphene.String) # Метод для разрешения значения поля 'tags' def resolve_tags(self, info): # Возвращаем все теги, связанные с объектом return self.tags.all() class WagtailImageNode(DjangoObjectType): # Определяем GraphQL-тип для модели Image из Wagtail class Meta: model = Image # Указываем интерфейс для работы с тегами interfaces = (TaggableInterface,) # Указываем, что поле 'tags' должно быть исключено из GraphQL-типов exclude = ['tags'] # Регистрируем конвертер для поля типа Image @convert_django_field.register(Image) def convert_image(field, registry=None): # Возвращаем WagtailImageNode как GraphQL тип для модели Image return WagtailImageNode( description=field.help_text, # Добавляем описание поля, если оно есть required=not field.null # Указываем, обязательно ли это поле )
Подробнее про интерфейсы в GraphQL можно прочитать тут.
# api/nodes/character.py # Определяем GraphQL-тип CharacterNode на основе Django-модели Character class CharacterNode(DjangoObjectType): # Поле episodes для получения списка эпизодов, связанных с персонажем episodes = graphene.List("project.api.EpisodeNode") # Поле location для получения информации о локации персонажа location = graphene.Field("project.api.LocationNode") # Поле image для получения данных об изображении image = graphene.Field("project.api.WagtailImageNode") class Meta: # Связываем GraphQL-тип с моделью Character model = Character # Указываем интерфейс для работы с тегами interfaces = (TaggableInterface,) # Указываем поля, которые должны быть доступны в запросах only_fields = ('name', 'status', 'species', 'gender', 'image', 'created', 'modified') # Исключаем поле 'tags', чтобы оно не было доступно в запросах exclude = ('tags',) # Метод для разрешения запроса на получение эпизодов, связанных с персонажем def resolve_episodes(self: Character, info): result = [] # Проходим по каждому эпизоду, связанному с персонажем for episode in self.episodes: # Добавляем идентификатор эпизода в результат result.append(episode.value) # Возвращаем список идентификаторов эпизодов return result # Метод для разрешения запроса на получение локации, связанной с персонажем def resolve_location(self: Character, info): # Возвращаем объект локации, связанный с персонажем return self.location
# api/nodes/episode.py # Определяем GraphQL-тип EpisodeNode на основе Django-модели Episode class EpisodeNode(DjangoObjectType): class Meta: # Связываем GraphQL-тип с моделью Episode model = Episode # Указываем интерфейс, который должен использоваться (в данном случае интерфейс для работы с тегами) interfaces = (TaggableInterface,) # Исключаем поле 'tags' из схемы GraphQL, чтобы оно не было доступно в запросах exclude = ('tags',) # Определяем поле character_set для получения списка персонажей, связанных с эпизодом character_set = graphene.List("project.api.CharacterNode") # Метод для разрешения запроса на получение списка персонажей, связанных с текущим эпизодом def resolve_character_set(self: Episode, info): # Фильтруем персонажей, которые связаны с эпизодом по его идентификатору return Character.objects.filter( episodes__contains=[{"value": self.id}] # Ищем персонажей, у которых эпизод присутствует в поле episodes )
# api/nodes/location.py # Определяем GraphQL-тип LocationNode на основе Django-модели Location class LocationNode(DjangoObjectType): class Meta: # Связываем GraphQL-тип с моделью Location model = Location # Указываем интерфейс, который должен использоваться (в данном случае интерфейс для работы с тегами) interfaces = (TaggableInterface,) # Исключаем поле 'tags' из схемы GraphQL, чтобы оно не было доступно в запросах exclude = ('tags',)
Queries:
Queries — это запросы, которые позволяют клиентам получать данные от сервера. Вот основные аспекты, которые помогут вам понять, что такое Queries и как они работают:
-
Определение запросов. Queries позволяют определить, какие данные можно запросить у сервера и в каком формате они будут возвращены. Каждый запрос в GraphQL представляет собой операцию, которая возвращает данные в виде структуры, соответствующей запросу. Например, запрос может быть использован для получения списка персонажей или конкретного персонажа по его ID.
-
Функция разрешения (Resolver). Для каждого запроса в GraphQL определен метод разрешения (resolver), который отвечает за извлечение данных из базы данных или другой системы и возвращение их в ответ на запрос. Метод разрешения выполняет логику, необходимую для выполнения запроса, например, фильтрацию данных или получение информации по заданным критериям.
-
Структура запросов. Запросы могут быть простыми и сложными, включать параметры для фильтрации данных или запрашивать связанные данные. А еще содержать поля, которые определяют, какие именно данные нужно вернуть.
Определим наши запросы:
# api/queries/character.py # Класс CharacterQuery будет содержать GraphQL запросы для работы с персонажами class CharacterQuery: # Запрос для получения списка персонажей с необязательным фильтром по статусу characters = graphene.List("project.api.CharacterNode", status=graphene.String(required=False)) # Запрос для получения конкретного персонажа по его ID character = graphene.Field("project.api.CharacterNode", character_id=graphene.Int(required=True)) # Метод для разрешения запроса на получение конкретного персонажа по его ID def resolve_character(self, info, character_id): try: # Пытаемся получить персонажа по ID return Character.objects.get(id=character_id) except Character.DoesNotExist: # Если персонаж с данным ID не найден, выбрасываем исключение raise Exception("Ошибка. Нет такого персонажа") # Метод для разрешения запроса на получение списка персонажей с фильтрацией по переданным аргументам def resolve_characters(self, info, **kwargs): # Создаем фильтр на основе переданных аргументов filters = Q(**kwargs) # Фильтруем список персонажей по условиям фильтра character = Character.objects.filter(filters) # Возвращаем отфильтрованный список персонажей return character
# api/queries/episode.py # Класс для выполнения запросов по эпизодам class EpisodeQuery: # Запрос для получения конкретного эпизода по его ID episode = graphene.Field("project.api.EpisodeNode", episode_id=graphene.Int(required=True)) # Запрос для получения списка эпизодов с возможностью фильтрации по датам episodes = graphene.List( "project.api.EpisodeNode", date_from=graphene.Date(required=False), # Дата начала диапазона date_to=graphene.Date(required=False) # Дата окончания диапазона ) # Метод для разрешения запроса на получение одного эпизода по его ID def resolve_episode(self, info, episode_id): try: # Пытаемся найти эпизод по его ID return Episode.objects.get(id=episode_id) except Episode.DoesNotExist: # Если эпизод не найден, выбрасываем исключение raise Exception("Ошибка. Нет такого эпизода") # Метод для разрешения запроса на получение списка эпизодов с фильтрацией по дате выпуска def resolve_episodes(self, info, date_from=None, date_to=None): # Создаем фильтр для фильтрации эпизодов по дате выпуска (air_date) filters = Q() # Если указана дата начала диапазона, добавляем фильтр на air_date ">= date_from" if date_from: filters &= Q(air_date__gte=date_from) # Если указана дата окончания диапазона, добавляем фильтр на air_date "<= date_to" if date_to: filters &= Q(air_date__lte=date_to) # Возвращаем отфильтрованный список эпизодов return Episode.objects.filter(filters)
# api/queries/locations.py # Класс для выполнения GraphQL-запросов по локациям class LocationQuery: # Запрос для получения конкретной локации по её ID location = graphene.Field("project.api.LocationNode", location_id=graphene.Int(required=True)) # Запрос для получения списка локаций с возможностью фильтрации по типу locations = graphene.List("project.api.LocationNode", type=graphene.String(required=False)) # Метод для разрешения запроса на получение одной локации по её ID def resolve_location(self, info, location_id): try: # Пытаемся найти локацию по её ID return Location.objects.get(id=location_id) except Location.DoesNotExist: # Если локация не найдена, выбрасываем исключение raise Exception("Ошибка. Нет такой локации") # Метод для разрешения запроса на получение списка локаций с возможностью фильтрации def resolve_locations(self, info, **kwargs): # Создаем фильтр на основе переданных аргументов (kwargs) filters = Q(**kwargs) # Применяем фильтр и возвращаем отфильтрованный список локаций return Location.objects.filter(filters)
Mutations:
Mutations — это операции, которые позволяют менять данные на сервере. В отличие от запросов (Queries), которые только получают данные, мутации их создают, обновляют и удаляют. Вот основные аспекты, которые помогут понять, что такое Mutations и как они работают:
-
Изменение данных. Mutations позволяют выполнять операции, которые меняют состояние данных на сервере. Например, вы можете использовать их для добавления нового персонажа, обновления информации о существующем или удаления персонажа из базы данных.
-
Определение мутаций. Каждая мутация определяет, какие данные должны изменить и вернуть после выполнения операции.
-
Методы разрешения (Resolvers). Для каждой мутации определен метод разрешения (resolver), который выполняет логику изменения данных. Этот метод обрабатывает входные данные, вносит изменения в базу и возвращает результат выполнения операции.
Определим наши мутации
# api/mutations/character.py class CharacterInput(graphene.InputObjectType): """ Входные данные для создания персонажа в GraphQL. """ name = graphene.String( required=True, description="Имя персонажа (обязательно)." # Описание для поля 'name', указывающее, что это обязательное поле ) status = graphene.String( required=True, description="Текущий статус персонажа (обязательно)." # Описание для поля 'status', обязательное ) species = graphene.String( required=True, description="Вид персонажа (обязательно)." # Описание для поля 'species', обязательное ) gender = graphene.String( required=True, description="Пол персонажа (обязательно)." # Описание для поля 'gender', обязательное ) tags = graphene.List( graphene.String, required=False, description="Дополнительные теги, связанные с персонажем (необязательно)." # Список тегов, необязательное поле ) location = graphene.InputField( LocationInput, required=True, description="Локация, связанная с персонажем (обязательно)." # Поле с локацией, обязательное ) episodes = graphene.List( EpisodeInput, required=True, description="Список эпизодов, в которых персонаж появлялся (обязательно)." # Обязательный список эпизодов ) class CreateCharacter(graphene.Mutation): """ Мутация для создания нового персонажа. """ class Arguments: character_input = CharacterInput( required=True, description="Данные о персонаже (обязательно)." # Аргумент мутации с данными о персонаже ) create = graphene.Field( graphene.Boolean, description="Указывает, был ли успешно создан персонаж." # Поле, которое показывает статус создания персонажа ) error_message = graphene.String( description="Сообщение об ошибке, если создание персонажа не удалось." # Поле для сообщения об ошибке ) character_data = graphene.Field( "project.api.CharacterNode", description="Данные о созданном персонаже." # Поле с данными о созданном персонаже ) location_data = graphene.Field( "project.api.LocationNode", description="Данные о локации, связанной с персонажем." # Поле с данными о локации персонажа ) episode_data = graphene.List( "project.api.EpisodeNode", description="Список эпизодов, связанных с персонажем." # Поле для списка связанных эпизодов ) # Основной метод для выполнения мутации создания персонажа @classmethod def mutate(cls, root, info, character_input: CharacterInput): """ Выполняет мутацию для создания персонажа. Args: root: Корневой резолвер. info: Контекст GraphQL. character_input (CharacterInput): Входные данные для персонажа. Returns: CreateCharacter: Результат мутации, включая статус успешности и данные. """ def _get_obj(model, get_field: dict = None, defaults: dict = None, tags: list = None): """ Вспомогательная функция для получения или создания объекта модели. Args: model: Модель для поиска или создания. get_field: Поля для поиска объекта. defaults: Значения по умолчанию, если объект нужно создать. tags: Теги для объекта, если они есть. Returns: obj: Найденный или созданный объект. """ obj, created = model.objects.get_or_create( **get_field, defaults=defaults ) if created and tags: obj.tags.add(*tags) # Добавляем теги, если объект был создан и теги присутствуют return obj try: # Получаем или создаем объект локации, основываясь на данных location_input: LocationInput = character_input.location location_obj: Location location_obj = _get_obj( Location, {"id": location_input.id} # Используем ID для поиска объекта ) if location_input.id else _get_obj( Location, {"name": location_input.name}, # Используем имя, если ID отсутствует { "type": location_input.type, "dimension": location_input.dimension, }, location_input.tags) # Заполняем поля для создания нового объекта # Создаем объект персонажа character = Character( name=character_input.name, status=character_input.status, species=character_input.species, gender=character_input.gender, location=location_obj # Привязываем локацию ) character.save() # Сохраняем персонажа в базе данных # Обработка списка эпизодов, в которых персонаж появлялся episodes_stream_data = [] episodes = [] for episode_input in character_input.episodes: # Получаем или создаем эпизоды episode = _get_obj( Episode, {'id': episode_input.id}, # Поиск эпизода по ID ) if episode_input.id else _get_obj( Episode, {"code": episode_input.code}, # Поиск эпизода по коду { "name": episode_input.name, 'air_date': episode_input.air_date }, episode_input.tags ) episodes_stream_data.append(('episode', episode)) # Добавляем эпизод в StreamField данные episodes.append(episode) # Добавляем эпизод в список # Преобразуем список эпизодов в StreamField episodes_stream_value = StreamValue(character.episodes.stream_block, episodes_stream_data) character.episodes = episodes_stream_value # Сохраняем эпизоды в объекте персонажа character.save() # Сохраняем персонажа после добавления эпизодов # Если указаны теги, добавляем их if character_input.tags: character.tags.add(*character_input.tags) # Добавляем теги к персонажу # Возвращаем успешный результат мутации create = True return CreateCharacter(create=create, character_data=character, location_data=location_obj, episode_data=episodes) except Exception as err: # Ловим любые другие ошибки и возвращаем их return CreateCharacter(create=False, error_message=str(err)) # Возвращаем ошибку в случае исключения # Объединяем мутации, связанные с персонажами class CharacterMutation: create_character = CreateCharacter.Field() # Определяем поле для создания персонажа в мутациях
# api/mutations/episode.py class EpisodeInput(graphene.InputObjectType): """ Входные данные для создания эпизода в GraphQL. """ id = graphene.Int( required=False, description="Идентификатор эпизода (необязательно)." ) name = graphene.String( required=False, description="Название эпизода (необязательно)." ) air_date = graphene.Date( required=False, description="Дата выхода эпизода (необязательно)." ) code = graphene.String( required=False, description="Код эпизода (необязательно)." ) tags = graphene.List( graphene.String, required=False, description="Дополнительные теги для эпизода (необязательно)." ) class CreateEpisode(graphene.Mutation): """ Мутация для создания нового эпизода. """ class Arguments: episode_data = EpisodeInput( required=True, description="Данные об эпизоде (обязательно)." ) create = graphene.Field( graphene.Boolean, description="Указывает, был ли успешно создан эпизод." ) error_message = graphene.String( description="Сообщение об ошибке, если создание эпизода не удалось." ) episode_data = graphene.Field( "project.api.EpisodeNode", description="Данные о созданном эпизоде." ) # Основной метод для выполнения мутации создания эпизода @classmethod def mutate(cls, root, info, episode_data): """ Выполняет мутацию для создания эпизода. Args: root: Корневой резолвер. info: Контекст GraphQL. episode_data (EpisodeInput): Входные данные для эпизода. Returns: CreateEpisode: Результат мутации, включая статус успешности и данные. """ try: # Создаем объект эпизода episode = Episode.objects.create( id=episode_data.id, name=episode_data.name, air_date=episode_data.air_date, code=episode_data.code ) # Если указаны теги, добавляем их if episode_data.tags: episode.tags.add(*episode_data.tags) # Возвращаем успешный результат мутации create = True return CreateEpisode(create=create, episode_data=episode) except IntegrityError: # Если эпизод с таким идентификатором уже существует, возвращаем ошибку return CreateEpisode(create=False, error_message="Такой эпизод уже существует в системе") except Exception as err: # Ловим любые другие ошибки и возвращаем их return CreateEpisode(create=False, error_message=str(err)) # Объединяем мутации, связанные с эпизодами class EpisodeMutation: create_episode = CreateEpisode.Field()
# api/mutations/location.py class LocationInput(graphene.InputObjectType): """ Входные данные для создания локации в GraphQL. """ id = graphene.Int( required=False, description="Идентификатор локации (необязательно)." ) name = graphene.String( required=False, description="Название локации (необязательно)." ) type = graphene.String( required=False, description="Тип локации (необязательно)." ) dimension = graphene.String( required=False, description="Размерность локации (необязательно)." ) tags = graphene.List( graphene.String, required=False, description="Дополнительные теги для локации (необязательно)." ) class CreateLocation(graphene.Mutation): """ Мутация для создания новой локации. """ class Arguments: location_data = LocationInput( required=True, description="Данные о локации (обязательно)." ) create = graphene.Field( graphene.Boolean, description="Указывает, была ли успешно создана локация." ) error_message = graphene.String( description="Сообщение об ошибке, если создание локации не удалось." ) location_data = graphene.Field( "project.api.LocationNode", description="Данные о созданной локации." ) # Основной метод для выполнения мутации создания локации @classmethod def mutate(cls, root, info, location_data): """ Выполняет мутацию для создания локации. Args: root: Корневой резолвер. info: Контекст GraphQL. location_data (LocationInput): Входные данные для локации. Returns: CreateLocation: Результат мутации, включая статус успешности и данные. """ try: # Создаем объект локации location = Location.objects.create( id=location_data.id, name=location_data.name, type=location_data.type, dimension=location_data.dimension ) # Если указаны теги, добавляем их if location_data.tags: location.tags.add(*location_data.tags) # Возвращаем успешный результат мутации create = True return CreateLocation(create=create, location_data=location) except IntegrityError: # Если локация с таким идентификатором уже существует, возвращаем ошибку return CreateLocation(create=False, error_message="Такая локация уже существует") except Exception as err: # Ловим любые другие ошибки и возвращаем их return CreateLocation(create=False, error_message=str(err)) # Объединяем мутации, связанные с локациями class LocationMutation: create_location = CreateLocation.Field()
Теперь посмотрим на результаты
Начнем с того, что база сейчас пуста и поэтому нам необходимо ее заполнить. Хорошо, что мы предусмотрели момент с отдельным созданием эпизодов и локаций, и их не придется создавать отдельными запросами. Мы можем сделать один запрос на создание персонажа, в котором мы укажем все необходимые данные.
Посмотрим на такую мутацию:
// наша мутация mutation createCharacter($character_input: CharacterInput!) { createCharacter(characterInput: $character_input) { create errorMessage locationData { name type dimension } episodeData { name code airDate } characterData { name status species gender location { name type dimension } episodes { name code airDate } } } }
// передаваемые значения - vaiables { "character_input": { "name": "Rick Sanchez", "status": "Alive", "species": "Human", "gender": "Male", "tags": [ "Scientist", "Adventurer" ], "location": { "name": "Earth", "type": "Planet", "dimension": "Dimension C-137", "tags": [ "Planet" ] }, "episodes": [ { "name": "Pilot", "airDate": "2013-12-02", "code": "S01E01", "tags": [ "First episode", "Season one" ] }, { "name": "Ricksy Business", "airDate": "2014-04-06", "code": "S01E11", "tags": [ "Eleven episode", "Season one" ] } ] } }
// ответ от сервера { "data": { "createCharacter": { "characterData": { "episodes": [ { "airDate": "2013-12-02", "code": "S01E01", "name": "Pilot" }, { "airDate": "2014-04-06", "code": "S01E11", "name": "Ricksy Business" } ], "gender": "Male", "location": { "dimension": "Dimension C-137", "name": "Earth", "type": "Planet" }, "name": "Rick Sanchez", "species": "Human", "status": "Alive" }, "create": true, "episodeData": [ { "airDate": "2013-12-02", "code": "S01E01", "name": "Pilot" }, { "airDate": "2014-04-06", "code": "S01E11", "name": "Ricksy Business" } ], "errorMessage": null, "locationData": { "dimension": "Dimension C-137", "name": "Earth", "type": "Planet" } } } }
Теперь если мы запросим всю информацию по персонажам:
// запрос всех персонажей query allCharacters { characters { episodes { airDate code name tags } gender image { file fileSize title } location { dimension name tags type } name species status tags } }
// ответ с сервера { "data": { "characters": [ { "episodes": [ { "airDate": "2013-12-02", "code": "S01E01", "name": "Pilot", "tags": [ "First episode", "Season one" ] }, { "airDate": "2014-04-06", "code": "S01E11", "name": "Ricksy Business", "tags": [ "Season one", "Eleven episode" ] } ], "gender": "Male", "image": null, "location": { "dimension": "Dimension C-137", "name": "Earth", "tags": [ "Planet" ], "type": "Planet" }, "name": "Rick Sanchez", "species": "Human", "status": "Alive", "tags": [ "Scientist", "Adventurer" ] } ] } }
Заключение
Итак, если вы смотрите на свой Wagtail и его обилие запросов с усталостью в глазах, возможно, вам пора познакомиться с GraphQL. Это как отправиться на фитнес для серверов: меньше тяжестей, больше гибкости. Вместо того, чтобы «тащить» избыточные данные, GraphQL позволяет взять только то, что в списке. Еще и сделает это изящно!
Поэтому, если вы мечтаете об ускорении работы вашего приложения и уменьшении его веса (в плане данных), GraphQL — ваш лучший друг! Если все еще сомневаетесь, не бойтесь рискнуть и протестировать, заслуживающих внимания технологий много, но таких, которые делают сервер счастливым — единицы.
И да, не забудьте оставить дверь открытой для REST, он все так же полезен и пригождается в огромном множестве проектов!
ссылка на оригинал статьи https://habr.com/ru/articles/845690/
Добавить комментарий