От REST к GraphQL: эволюция управления данными в Wagtail

от автора

В системах управления контентом (или 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 же позволяет выбирать только действительно нужные данные, даже если они сложны и вложены. Хотите получить только название рецепта? Легко. Нужен полный комплект данных с фото, описанием и видео? Проще говоря, это — «шведский стол», на котором выбираешь только нужное.

А здесь собраны все различия между REST API и GraphQL.

А здесь собраны все различия между REST API и GraphQL.

Конечно, не каждый проект нужно срочно переводить на GraphQL. Если ваше приложение работает стабильно, запросов немного и данные не слишком динамичные, REST API вполне справится. Но если сервис растет, запросы становятся сложнее, и хочется оптимизировать работу с данными — самое время пересесть с «велосипеда» REST на GraphQL, чтобы быстро и легко «маневрировать» в мире динамически меняющихся данных.

Практика. Создание приложения и работа с GraphQL

Я обещал показать весь процесс настройки системы. Вот что мы сделаем:

  1. Создадим простое приложение на Wagtail, где будем работать с персонажами, локациями и эпизодами. Это поможет увидеть, как GraphQL упрощает работу с данными.

  2. Настроим GraphQL в Django-проекте: установим необходимые инструменты, чтобы GraphQL мог работать с нашим приложением.

  3. Напишем запросы и мутации. Я покажу, как создавать запросы для получения данных и мутации для их изменения, чтобы управлять данными было легко.

  4. Научимся использовать интерфейс GraphQL для тестирования и отладки GraphQL-запросов. Это поможет проверить, что все работает правильно.

В результате вы получите практический опыт работы с GraphQL и сможете перенести его на свои проекты.

Разбираемся с настройкой

Для начала определимся со структурой моделей в нашем приложении

Character — данные по персонажам.Location — данные по локациям.Episode — данные по эпизодам.

Character — данные по персонажам.
Location — данные по локациям.
Episode — данные по эпизодам.

Если у вас еще нет приложения на 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). 

Структура модуля:

mutations — хранит все мутации схемы.nodes — хранит все описания моделей схемы.queries — хранит все запросы схемы.

mutations хранит все мутации схемы.
nodes хранит все описания моделей схемы.
queries — хранит все запросы схемы.

В 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 и как он работает:

  1. Типизация и моделирование. Node — это GraphQL-тип, который определяет, какие поля и типы данных доступны для запросов. Например, если у вас есть CharacterNode, это значит, что он представляет данные о персонаже и описывает, какие поля доступны для запроса, такие как имя, статус и изображение.

  2. Связь с Django. Node связывает модель Django с GraphQL-схемой. Он преобразует данные модели Django в формат, понятный GraphQL. Это позволяет GraphQL-запросам обращаться к данным Django-моделей без необходимости вручную обрабатывать их.

  3. Разрешители (Resolvers). Каждый Node может иметь методы, называемые разрешителями, которые определяют, как получать данные для каждого поля. Эти методы отвечают за выполнение запросов и возврат нужных данных. Например, resolve_episodes в CharacterNode возвращает список эпизодов, связанных с персонажем.

  4. Связи и Поля. 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 и как они работают:

  1. Определение запросов. Queries позволяют определить, какие данные можно запросить у сервера и в каком формате они будут возвращены. Каждый запрос в GraphQL представляет собой операцию, которая возвращает данные в виде структуры, соответствующей запросу. Например, запрос может быть использован для получения списка персонажей или конкретного персонажа по его ID.

  2. Функция разрешения (Resolver). Для каждого запроса в GraphQL определен метод разрешения (resolver), который отвечает за извлечение данных из базы данных или другой системы и возвращение их в ответ на запрос. Метод разрешения выполняет логику, необходимую для выполнения запроса, например, фильтрацию данных или получение информации по заданным критериям.

  3. Структура запросов. Запросы могут быть простыми и сложными, включать параметры для фильтрации данных или запрашивать связанные данные. А еще содержать поля, которые определяют, какие именно данные нужно вернуть.

Определим наши запросы:

# 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 и как они работают:

  1. Изменение данных. Mutations позволяют выполнять операции, которые меняют состояние данных на сервере. Например, вы можете использовать их для добавления нового персонажа, обновления информации о существующем или удаления персонажа из базы данных.

  2. Определение мутаций. Каждая мутация определяет, какие данные должны изменить и вернуть после выполнения операции. 

  3. Методы разрешения (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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *