В системах управления контентом (или 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, он все так же полезен и пригождается в огромном множестве проектов!
ggo
Вся преимущество GraphQL в федерации.
Когда у вас микросервисы, и хочется иметь одну точку управления схемами со всех микросервисов. Вот тогда, да, GraphQL проявляется во всей красе. Правда не достается бесплатно, есть свои ограничения.
Если у вас один сервис, то особого преимущества REST vs GQL нет.