Tutorial

Создание коротких ссылок с помощью Django и GraphQL

Published on April 29, 2020
Русский
Создание коротких ссылок с помощью Django и GraphQL

Автор выбрал Girls Who Code для получения пожертвования в рамках программы Write for DOnations.

Введение

GraphQL — это стандарт API с открытым исходным кодом, созданный Facebook в качестве альтернативы REST API. В отличие от REST API, GraphQL использует типизированную систему для определения структуры данных, где вся отправляемая и получаемая информация должна соответствовать предварительно заданной схеме. Также он подразумевает использование одной конечной точки для любых коммуникаций вместо использования нескольких URL-адресов для разных ресурсов, а также позволяет решить проблему оверфетчинга, возвращая только запрошенные клиентом данные, генерируя более компактные и понятные ответы.

В этом обучающем руководстве вы сможете создать серверную часть для службы коротких ссылок, которая принимает любой URL-адрес и генерирует более короткую и понятную версию, а также познакомиться с такими понятиями GraphQL, как запросы и мутации, и инструментами, например, интерфейсом GraphiQL. Возможно, вы уже использовали ранее такие сервисы, как bit.ly.

Поскольку GraphQL является независимой от языка технологией, она реализуется поверх самых разных языков и фреймворков. Здесь вы будете использовать универсальный язык программирования Python, веб-фреймворк Django и библиотеку Graphene-Django в качестве имплементации Python для GraphQL с конкретными интеграциями для Django.

Предварительные требования

  • Для продолжения выполнения этого руководства вам потребуется версия Python 3.5 или выше, установленная на вашем компьютере для разработки. Для установки Python воспользуйтесь нашим руководством Установка и настройка локальной среды программирования для Python 3 для вашей ОС. Обязательно создайте и запустите виртуальную среду; чтобы выполнять указания этого обучающего руководства, вы можете использовать директорию проекта shorty.

  • Желательно наличие базового знания Django, но это необязательно. Если вам интересно, вы можете воспользоваться этой серией материалов о разработке в Django, созданной сообществом DigitalOcean.

Шаг 1 — Настройка проекта Django

На этом шаге вы выполните установку всех необходимых инструментов для приложения и настроите ваш проект Django.

После создания директории проекта и запуска вашей виртуальной среды, как указано в предварительных условиях, установите необходимые пакеты с помощью pip, диспетчера пакетов Python. В этом обучающем руководстве используется версия Django 2.1.7 и версия Graphene-Django 2.2.0 или выше:

  1. pip install "django==2.1.7" "graphene-django>==2.2.0"

Теперь у вас есть все необходимые инструменты. Далее вам необходимо создать проект Django, используя команду django-admin. Проект представляет собой стандартный шаблон Django по умолчанию, т. е. набор папок и файлов со всем необходимым для начала разработки веб-приложения. В этом случае вы можете вызвать ваш проект shorty и создать его в текущей папке, указав в конце .:

  1. django-admin startproject shorty .

После создания вашего проекта выполните запуск миграции Django. Эти файлы содержат код Python, сгенерированный Django, и несут ответственность за изменение структуры приложения в соответствии с моделями Django. Изменения могут, например, включать создание таблицы. По умолчанию Django имеет собственный набор миграций, отвечающий за такие подсистемы, как аутентификация Django, поэтому необходимо выполнить их с помощью следующей команды:

  1. python manage.py migrate

Эта команда использует интерпретатор Python для вызова скрипта Django с именем manage.py, который отвечает за управление различными аспектами вашего проекта, например, созданием приложений или запуском миграции.

Результат будет выглядеть примерно следующим образом:

Output
Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying sessions.0001_initial... OK

Когда база данных Django будет готова, запустите локальный сервер разработки:

  1. python manage.py runserver

Это даст нам следующее:

Output
Performing system checks... System check identified no issues (0 silenced). March 18, 2020 - 15:46:15 Django version 2.1.7, using settings 'shorty.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.

Эта команда будет отменять запрос в вашем терминале и запускать сервер.

Перейдите на страницу http://127.0.0.1:8000 в вашем браузере. Вы увидите следующую страницу:

Стартовая страница локального сервера Django

Чтобы остановить сервер и вернуться к терминалу, нажмите CTRL+C. Когда вам потребуется получить доступ к браузеру, убедитесь, что предыдущая команда запущена.

Далее мы закончим выполнение этого шага, активировав библиотеку Django-Graphene в проекте. Django использует концепцию app, т. е. веб-приложения с конкретной ответственностью. Проект включает одно или несколько приложений. Откройте файл shorty/settings.py в текстовом редакторе по вашему выбору. В этом обучающем руководстве мы будем использовать vim:

  1. vim shorty/settings.py

Файл settings.py управляет всеми параметрами вашего проекта. Внутри файла найдите запись INSTALLED_APPS и добавьте строку 'graphene_django':

shorty/shorty/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'graphene_django',
]
...

Это добавление указывает Django, что вы будете использовать приложение с именем graphene_django, которое вы установили на шаге 1.

Добавьте внизу файла следующую переменную:

shorty/shorty/settings.py
...
GRAPHENE = {
    'SCHEMA': 'shorty.schema.schema',
}

Последняя переменная указывает на вашу основную схему, которую вы создадите позже. В GraphQL схема содержит все типы объектов, такие как ресурсы, запросы и мутации. Вы можете рассматривать ее как документацию, которая представляет все данные и функционал, доступный в вашей системе.

После внесения изменений сохраните и закройте файл.

Вы закончили настройку проекта Django. На следующем шаге мы создадим приложение Django и модели.

Шаг 2 — Настройка приложения Django и моделей

Платформа Django обычно включает один проект и несколько приложений или app. App описывает набор функций внутри проекта, и при правильной разработке может повторно использоваться в других проектах Django.

На этом шаге мы создадим приложение shortener​​​, которое отвечает за фактическое укорачивание URL-адреса. Для создания базового каркаса введите следующую команду в терминале:

  1. python manage.py startapp shortener

Здесь вы использовали параметры startapp app_name​​, которые указывают manage.py​​​ создать приложение с именем shortener.

Чтобы завершить процесс создания приложения, откройте файл shorty/settings.py.

  1. vim shorty/settings.py

Добавьте имя приложения в ту же запись INSTALLED_APPS, которую вы изменили ранее:

shorty/shorty/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'graphene_django'
    'shortener',
]
...

Сохраните и закройте файл.

После добавления shortener в shorty/settings.py вы можете перейти к созданию моделей вашего проекта. Модели — одна из ключевых функций в Django. Они используются для представления базы данных в используемом в Python образе, что позволяет управлять, запрашивать и сохранять данные с помощью кода Python.

Прежде чем открыть файл models.py для внесения изменений, в этом обучающем руководстве мы разместим обзор изменений, которые вы вносите.

Ваш файл модели —shortener/models.py — будет содержать следующее содержание, после того как вы заменили существующий код:

shorty/shortener/models.py
from hashlib import md5

from django.db import models

Здесь вы импортируете требуемые пакеты, необходимые вашему коду. Вам нужно добавить строку from hashlib import md5 в верхней части для импорта стандартной библиотеки, которая будет использоваться для создания хэша URL-адреса. Строка from django.db import models — это элемент Django для создания моделей.

Предупреждение. В этом обучающем руководстве хэш является результатом выполнения функции, которая получает данные и всегда возвращает один вывод. В этом обучающем руководстве мы будем использовать хэш-функцию MD5 в демонстрационных целях.

Обратите внимание, что MD5 имеет проблемы с коллизиями, поэтому рекомендуется избегать ее использования в производственной среде.

Далее вам необходимо добавить модель с именем URL и следующими полями:

  • full_url: URL для сокращения.
  • url_hash: краткий хэш, представляющий полный URL.
  • clicks: сколько раз был использован короткий URL.
  • created_at: дата и время создания URL.
shorty/shortener/models.py
...

class URL(models.Model):
    full_url = models.URLField(unique=True)
    url_hash = models.URLField(unique=True)
    clicks = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

Вы сгенерируете url_hash, применив алгоритм хеширования MD5 для поля full_url​​​ и используя только первые 10 символов, которые возвращает метод save() модели, выполняемый каждый раз, когда Django сохраняет запись в базе данных. Кроме того, инструменты для сокращения URL-адреса обычно отслеживают, сколько раз была использована ссылка. Вы можете вызвать для этого метод clicked()​​, когда URL используется пользователем.

Упомянутые операции будут добавлены в вашу модель URL с помощью этого кода:

shorty/shortener/models.py
...

    def clicked(self):
        self.clicks += 1
        self.save()

    def save(self, *args, **kwargs):
        if not self.id:
            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]

        return super().save(*args, **kwargs)

Теперь, когда вы просмотрели код, откройте файл shortener/models.py:

  1. vim shortener/models.py

Замените код на следующий:

shorty/shortener/models.py
from hashlib import md5

from django.db import models


class URL(models.Model):
    full_url = models.URLField(unique=True)
    url_hash = models.URLField(unique=True)
    clicks = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    def clicked(self):
        self.clicks += 1
        self.save()

    def save(self, *args, **kwargs):
        if not self.id:
            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]

        return super().save(*args, **kwargs)

Обязательно сохраните и закройте файл.

Для применения этих изменений в базе данных вам потребуется создать миграции, запустив следующую команду:

  1. python manage.py makemigrations

Результат будет выглядеть следующим образом:

Output
Migrations for 'shortener': shortener/migrations/0001_initial.py - Create model URL

Затем выполните миграции:

  1. python manage.py migrate

В своем терминале вы увидите следующее:

Output
Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, shortener Running migrations: Applying shortener.0001_initial... OK

Теперь, когда вы настроили модели, на следующем шаге мы создадим конечную точку GraphQL и запрос.

Шаг 3 — Создание запросов

Архитектура REST предоставляет разные ресурсы в разных конечных точках, каждый из которых содержит четко определенную структуру данных. Например, вы можете получить список пользователей в /api/users, который всегда ожидает одни и те же поля. GraphQL, с другой стороны, имеет одну конечную точку для всех взаимодействий и использует запросы для доступа к данным. Главное, и самое ценное, различие заключается в том, что вы можете использовать запрос для получения всех ваших пользователей с помощью одного запроса.

Начните с создания запроса для получения всех URL. Вам потребуется несколько вещей:

  • Тип URL, который привязан к ранее определенной модели.
  • Оператор запроса с именем urls.
  • Метод для обработки вашего запроса, т. е. получения всех URL из базы данных и их возвращения клиенту.

Создайте новый файл shortener/schema.py:

  1. vim shortener/schema.py

Начнем с добавления оператора import Python:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType

from .models import URL

Первая строка импортирует основную библиотеку graphene, которая содержит базовые типы GraphQL, например List. DjangoObjectType — это вспомогательный метод для создания определения схемы из любой модели Django, а третья строка импортирует ранее созданную модель URL.

Создайте новый тип GraphQL для модели URL, добавив следующие строки:

shorty/shortener/schema.py
...
class URLType(DjangoObjectType):
    class Meta:
        model = URL

В заключение добавьте эти строки для создания типа запроса для модели URL:

shorty/shortener/schema.py
...
class Query(graphene.ObjectType):
    urls = graphene.List(URLType)

    def resolve_urls(self, info, **kwargs):
        return URL.objects.all()

Этот код создает класс Query с одним полем urls, который представляет собой список с ранее определенным типом URLType. При обработке запроса с помощью метода resolve_urls вы возвращаете все URL, сохраненные в базе данных.

Полное содержание файла shortener/schema.py показано здесь:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType

from .models import URL


class URLType(DjangoObjectType):
    class Meta:
        model = URL


class Query(graphene.ObjectType):
    urls = graphene.List(URLType)

    def resolve_urls(self, info, **kwargs):
        return URL.objects.all()

Сохраните и закройте файл.

Теперь все запросы необходимо добавить в основную схему. Рассматривайте ее как место хранения всех ваших ресурсов.

Создайте новый файл в shorty/schema.py и откройте его в редакторе:

  1. vim shorty/schema

Импортируйте пакеты Python, добавив следующие строки. Первая, как уже упоминалось, содержит базовые типы GraphQL. Вторая строка импортирует ранее созданный файл схемы.

shorty/shorty/schema.py
import graphene

import shortener.schema

Затем добавьте главный класс Query. Он будет хранить, через наследование, все запросы и будущие операции:

shorty/shorty/schema.py
...
class Query(shortener.schema.Query, graphene.ObjectType):
    pass

В заключение создайте переменную schema:

shorty/shorty/schema.py
...
schema = graphene.Schema(query=Query)

Настройка SCHEMA, которую вы задали на шаге 2, указывает на переменную schema, которую вы только что создали.

Полное содержание файла shorty/schema.py​​​ показано здесь:

shorty/shorty/schema.py
import graphene

import shortener.schema


class Query(shortener.schema.Query, graphene.ObjectType):
    pass

schema = graphene.Schema(query=Query)

Сохраните и закройте файл.

Затем активируйте конечную точку GraphQL и интерфейс GraphiQL, который представляет собой графический веб-интерфейс, который используется для взаимодействия с системой GraphQL.

Откройте файл shorty/urls.py​​​:

  1. vim shorty/urls.py

В целях обучения удалите содержимое файла и сохраните его, чтобы начать с нуля.

Первые строки, которые нужно добавить, объявляют импорт Python:

shorty/shorty/urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from graphene_django.views import GraphQLView

Функция path используется Django для создания доступного URL-адреса для интерфейса GraphiQL. Затем импортируйте csrf_exempt, который позволяет клиентам отправлять данные на сервер. Полное объяснение можно найти в документации по Graphene. В последней строке вы импортировали реальный код, который отвечает за интерфейс, через GraphQLView.

Затем создайте переменную urlpatterns.

shorty/shorty/urls.py
...
urlpatterns = [
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

Она будет связывать весь код, который необходим для создания интерфейса GraphiQL, который будет доступен в пути graphql/:

Полное содержание файла shortener/urls.py показано здесь:

shorty/shorty/urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from graphene_django.views import GraphQLView

urlpatterns = [
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

Сохраните и закройте файл.

Вернитесь в терминал, запустите команду python manage.py runserver (если она еще не запущена):

  1. python manage.py runserver

Откройте в браузере адрес http://localhost:8000/graphql​​​. Вы увидите следующее:

Интерфейс GraphiQL

GraphiQL — это интерфейс, где вы можете запускать операторы GraphQL и просматривать результаты. Одна из функций — раздел Docs в правом верхнем углу. Поскольку все в GraphQL типизировано, вы получите свободную документацию обо всех ваших типах, запросах, мутациях и т. д.

После изучения страницы вставьте ваш первый запрос в основную текстовую область:

query {
  urls {
    id
    fullUrl
    urlHash
    clicks
    createdAt
  }
}

Это содержание показывает, какую структуру имеет запрос GraphQL: сначала вы используете ключевое слово query, чтобы указать серверу, что вы хотите получить только определенные данные. Далее мы используем поле urls, определенное в файле shortener/schema.py, внутри класса Query. С помощью этого действия вы явно запрашиваете все поля, определенные в модели URL, используя «верблюжий» стиль, используемый по умолчанию в GraphQL.

Теперь нажмите кнопку запуска со стрелкой в левом верхнем углу.

Вы получите следующий ответ, указывающий, что у вас все еще нет URL-адресов:

Output
{ "data": { "urls": [] } }

Это показывает, что GraphQL работает. В терминале нажмите CTRL+C, чтобы остановить ваш сервер.

Вы выполнили большой объем работы на этом шаге, создав конечную точку GraphQL, создав запрос для получения всех URL-адресов и активировав интерфейс GraphiQL. Теперь мы создадим мутации для изменения базы данных.

Шаг 4 — Создание мутаций

Большинство приложений имеют возможность изменять состояние базы данных, добавляя, обновляя или удаляя данные. В GraphQL эти операции называются мутациями. Они похожи на запросы, но используют аргументы для отправки данных на сервер.

Для создания первой мутации откройте shortener/schema.py​​​:

  1. vim shortener/schema.py

В конце файла добавьте новый класс CreateURL:

shorty/shortener/schema.py
...
class CreateURL(graphene.Mutation):
    url = graphene.Field(URLType)

Этот класс наследует вспомогательный класс graphene.Mutation для использования возможностей мутаций GraphQL. Также у него есть имя свойства url, определяющее содержание, возвращаемое сервером после завершения мутации. В этом случае это структура данных с типом URLType.

Затем добавьте подкласс с именем Arguments в уже определенный класс:

shorty/shortener/schema.py
...
    class Arguments:
        full_url = graphene.String()

Он определяет, какие данные будут приниматься сервером. Здесь вы ожидаете параметр full_url со строковым содержанием:

Теперь добавьте следующие строки для создания метода mutate:

shorty/shortener/schema.py
...

    def mutate(self, info, full_url):
        url = URL(full_url=full_url)
        url.save()

Этот метод mutate выполняет большой объем работы, получая данные от клиента и сохраняя их в базу данных. В результате он возвращает сам класс, содержащий вновь созданный элемент.

В заключение создайте класс Mutation для хранения всех мутаций вашего приложения, добавив следующие строки:

shorty/shortener/schema.py
...

class Mutation(graphene.ObjectType):
    create_url = CreateURL.Field()

Пока у вас будет только одна мутация с именем create_url.

Полное содержание файла shortener/schema.py показано здесь:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType

from .models import URL


class URLType(DjangoObjectType):
    class Meta:
        model = URL


class Query(graphene.ObjectType):
    urls = graphene.List(URLType)

    def resolve_urls(self, info, **kwargs):
        return URL.objects.all()


class CreateURL(graphene.Mutation):
    url = graphene.Field(URLType)

    class Arguments:
        full_url = graphene.String()

    def mutate(self, info, full_url):
        url = URL(full_url=full_url)
        url.save()

        return CreateURL(url=url)


class Mutation(graphene.ObjectType):
    create_url = CreateURL.Field()

Закройте и сохраните файл.

Чтобы завершить добавление мутации, измените файл shorty/schema.py​​​:

  1. vim shorty/schema.py

Измените файл, чтобы добавить в него следующий выделенный код:

shorty/shorty/schema.py

import graphene

import shortener.schema


class Query(shortener.schema.Query, graphene.ObjectType):
    pass


class Mutation(shortener.schema.Mutation, graphene.ObjectType):
    pass


schema = graphene.Schema(query=Query, mutation=Mutation)

Сохраните и закройте файл. Если ваш локальный сервер не запущен, запустите его:

  1. python manage.py runserver

Перейдите на страницу http://localhost:8000/graphql​​​ в браузере. Выполните вашу первую мутацию в веб-интерфейсе GraphiQL, запустив следующее выражение:

mutation {
  createUrl(fullUrl:"https://www.digitalocean.com/community") {
    url {
      id
      fullUrl
      urlHash
      clicks
      createdAt
    }
  }
}

Вы создали мутацию с именем createURL, аргументом fullUrl и данными, которые хотите получить в ответ, определенные в поле url.

Вывод будет содержать информацию о URL, которую вы только что создали в поле data GraphQL, как показано здесь:

Output
{ "data": { "createUrl": { "url": { "id": "1", "fullUrl": "https://www.digitalocean.com/community", "urlHash": "077880af78", "clicks": 0, "createdAt": "2020-01-30T19:15:10.820062+00:00" } } } }

В результате URL был добавлен в базу данных с хэшированной версией, как вы можете видеть в поле urlHash. Попробуйте запустить запрос, который вы создали в последнем шаге, чтобы увидеть результат:

query {
  urls {
    id
    fullUrl
    urlHash
    clicks
    createdAt
  }
}

Вывод будет показывать сохраненный URL:

Output
{ "data": { "urls": [ { "id": "1", "fullUrl": "https://www.digitalocean.com/community", "urlHash": "077880af78", "clicks": 0, "createdAt": "2020-03-18T21:03:24.664934+00:00" } ] } }

Также вы можете попробовать выполнить тот же запрос, но на этот раз запросить только поля, которые хотите получить.

Попробуйте выполнить его еще раз с другим URL-адресом:

mutation {
  createUrl(fullUrl:"https://www.digitalocean.com/write-for-donations/") {
    url {
      id
      fullUrl
      urlHash
      clicks
      createdAt
    }
  }
}

Результат будет выглядеть следующим образом:

Output
{ "data": { "createUrl": { "url": { "id": "2", "fullUrl": "https://www.digitalocean.com/write-for-donations/", "urlHash": "703562669b", "clicks": 0, "createdAt": "2020-01-30T19:31:10.820062+00:00" } } } }

Теперь система может создавать короткие URL-адреса и выводить их список. На следующем шаге мы будем предоставлять пользователям доступ к URL по его сокращенной версии, перенаправляя их на верную страницу.

Шаг 5 — Создание конечной точки доступа

На этом шаге мы будем использовать метод Django Views, который выполняет запрос и возвращает ответ для перенаправления любого, кто пытается получить доступ через конечную точку http://localhost:8000/url_hash​​, на полный URL-адрес.

Откройте файл shortener/views.py в редакторе:

  1. vim shortener/views.py

Вначале импортируйте два пакета, заменив содержимое на следующие строки:

shorty/shortener/views.py
from django.shortcuts import get_object_or_404, redirect

from .models import URL

Это будет более подробно разъяснено позднее.

Далее мы создадим представление Django с именем root. Добавьте этот отрывок кода, который будет отвечать за представление, в конец вашего файла:

shorty/shortener/views.py
...

def root(request, url_hash):
    url = get_object_or_404(URL, url_hash=url_hash)
    url.clicked()

    return redirect(url.full_url)

Он получает аргумент с именем url_hash из URL, запрошенного пользователем. Внутри функции первая строка пытается получить URL из базы данных, используя аргумент url_hash. Если он не будет найден, клиент получает ошибку 404, что означает, что ресурс отсутствует. Затем он увеличивает значение свойства clicked для URL, что позволяет отслеживать, сколько раз был использован URL. В конце он перенаправляет клиента на запрошенный URL-адрес.

Полное содержание файла shortener/views.py показано здесь:

shorty/shortener/views.py
from django.shortcuts import get_object_or_404, redirect

from .models import URL


def root(request, url_hash):
    url = get_object_or_404(URL, url_hash=url_hash)
    url.clicked()

    return redirect(url.full_url)

Сохраните и закройте файл.

Откройте shorty/urls.py​​:

  1. vim shorty/urls.py

Добавьте следующий выделенный код, чтобы активировать представление root.

shorty/shorty/urls.py

from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from graphene_django.views import GraphQLView

from shortener.views import root


urlpatterns = [
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
    path('<str:url_hash>/', root, name='root'),
]

Представление root будет доступно в пути / вашего сервера, принимая url_hash в качестве строкового параметра.

Сохраните и закройте файл. Если ваш локальный сервер не запущен, запустите его с помощью команды python manage.py runserver.

Чтобы протестировать ваши добавленные данные, откройте ваш браузер и перейдите на URL-адрес http://localhost:8000/077880af78. Обратите внимание, что последняя часть URL-адреса — это хэш, созданный мутацией из шага 5. Вы будете перенаправлены на страницу хэша URL, в этом случае на веб-сайт сообщества DigitalOcean.

Теперь, когда у вас есть работающее перенаправление URL, мы обеспечим дополнительную безопасность приложения, реализовав обработку ошибок при выполнении мутации.

Шаг 6 — Реализация обработки ошибок

Обработка ошибок — это лучшая практика для всех приложений, поскольку разработчики обычно не контролируют, что будет отправляться на сервер. В этом случае вы можете попытаться предусмотреть возможные ошибки и свести к минимуму их воздействие. В такой сложной системе, как GraphQL, очень многое может пойти не так, начиная с запроса неверных данных клиентом и заканчивая потерей подключения сервера к базе данных.

Будучи типизированной системой, GraphQL может проверять все, что запрашивает и получает клиент, с помощью операции под названием валидация схемы. Вы можете посмотреть это в действии, используя запрос несуществующего поля.

Перейдите на страницу http://localhost:8000/graphql в браузере еще раз и выполните следующий запрос внутри интерфейса GraphiQL, где мы будем использовать поле iDontExist:

query {
  urls {
    id
    fullUrl
    urlHash
    clicks
    createdAt
    iDontExist
  }
}

Поскольку в классе Query отсутствует поле iDontExist, GraphQL возвращает сообщение об ошибке:

Output
{ "errors": [ { "message": "Cannot query field \"iDontExist\" on type \"URLType\".", "locations": [ { "line": 8, "column": 5 } ] } ] }

Это важно, поскольку в типизированной системе GraphQL цель заключается в отправке и получении только той информации, которая уже определена в схеме.

Текущее приложение принимает любую произвольную строку в поле full_url. Проблема в том, что если кто-то отправляет плохо построенный URL-адрес, вы можете перенаправлять пользователя в никуда при попытке получения сохраненной информации. В этом случае вам необходимо проверить, имеет ли full_url​​​ корректный формат, прежде чем сохранить его в базе данных, и при наличии ошибки выбрасывать исключение GraphQLError с заданным сообщением.

Давайте реализуем этот функционал в два этапа. Вначале откройте файл shortener/models.py​​​:

  1. vim shortener/models.py

Добавьте выделенные строки в раздел импорта:

shorty/shortener/models.py
from hashlib import md5

from django.db import models
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError

from graphql import GraphQLError
...

URLValidator — это вспомогательный класс Django для валидации строки URL, а GraphQLError используется Graphene для генерации исключений с заданным сообщением.

Затем необходимо выполнить валидацию URL-адреса, который получает пользователь, прежде чем сохранить его в базе данных. Активируйте эту операцию, добавив выделенный код в файл shortener/models.py:

shorty/shortener/models.py
class URL(models.Model):
    full_url = models.URLField(unique=True)
    url_hash = models.URLField(unique=True)
    clicks = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    def clicked(self):
        self.clicks += 1
        self.save()

    def save(self, *args, **kwargs):
        if not self.id:
            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]

        validate = URLValidator()
        try:
            validate(self.full_url)
        except ValidationError as e:
            raise GraphQLError('invalid url')

        return super().save(*args, **kwargs)

Сначала этот код инициализирует URLValidator в переменной validate. Внутри блока try/except​​​ вы с помощью метода validate()​​​ выполняете валидацию полученного URL-адреса и генерируете исключение GraphQLError с заданным сообщением invalid url​​​, если что-то пойдет не так.

Полное содержание файла shortener/models.py показано здесь:

shorty/shortener/models.py
from hashlib import md5

from django.db import models
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError

from graphql import GraphQLError


class URL(models.Model):
    full_url = models.URLField(unique=True)
    url_hash = models.URLField(unique=True)
    clicks = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    def clicked(self):
        self.clicks += 1
        self.save()

    def save(self, *args, **kwargs):
        if not self.id:
            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]

        validate = URLValidator()
        try:
            validate(self.full_url)
        except ValidationError as e:
            raise GraphQLError('invalid url')

        return super().save(*args, **kwargs)

Сохраните и закройте файл. Если ваш локальный сервер не запущен, запустите его с помощью команды python manage.py runserver.

Затем протестируйте вашу обработку ошибок на странице http://localhost:8000/graphql. Попробуйте создать новый URL с недействительным значением full_url​​​ в интерфейсе GraphiQL:

mutation {
  createUrl(fullUrl:"not_valid_url"){
    url {
      id
      fullUrl
      urlHash
      clicks
      createdAt
    }
  }
}

При отправке недействительного URL-адреса будет сгенерировано исключение с заданным сообщением:

Output
{ "errors": [ { "message": "invalid url", "locations": [ { "line": 2, "column": 3 } ], "path": [ "createUrl" ] } ], "data": { "createUrl": null } }

Если вы посмотрите в терминале, где запущена команда python manage.py runserver, ошибка будет выглядеть следующим образом:

Output
... graphql.error.located_error.GraphQLLocatedError: invalid url [30/Jan/2020 19:46:32] "POST /graphql/ HTTP/1.1" 200 121

Конечная точка GraphQL будет выдавать сбой с кодом состояния HTTP 200, который обычно означает успешное выполнение операции. Не забывайте, что, хотя GraphQL использует HTTP, он не обязательно использует концепции кодов состояния HTTP или методы HTTP, как это делает REST.

После реализации обработки ошибок вы можете создать механизм для фильтрации запросов, минимизируя объем информации, получаемый пользователем.

Шаг 7 — Внедрение фильтров

Представьте, что вы начали использовать инструмент для получения коротких URL-адресов для добавления собственных ссылок. После этого появится очень большое количество записей, так что найти нужную ссылку будет сложно. Вы можете решить эту проблему, используя фильтры.

Фильтрация — это стандартная концепция в REST API, когда параметр запроса с полем и значением присоединяется к URL-адресу. Например, чтобы воспользоваться фильтром для всех пользователей с именем jojo, вы можете использовать GET /api/users?name=jojo.

В GraphQL вы будете использовать аргументы запроса в качестве фильтров. Они позволяют получить хороший и чистый интерфейс.

Вы можете решить проблему трудноуловимых URL-адресов, разрешив клиенту фильтровать URL-адреса по имени с помощью поля full_url. Для этого откройте файл shortener/schema.py в предпочитаемом вами редакторе.

  1. vim shortener/schema.py

Вначале импортируйте метод Q в выделенной строке:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType
from django.db.models import Q

from .models import URL
...

Это будет использоваться для фильтрации запроса базы данных.

Затем перепишите весь класс Query, добавив следующее содержание:

shorty/shortener/schema.py
...
class Query(graphene.ObjectType):
    urls = graphene.List(URLType, url=graphene.String())

    def resolve_urls(self, info, url=None, **kwargs):
        queryset = URL.objects.all()

        if url:
            _filter = Q(full_url__icontains=url)
            queryset = queryset.filter(_filter)

        return queryset
...

Ниже представлены изменения, которые вы вносите:

  • Добавление параметра фильтрации url внутри переменной urls и метод resolve_url.
  • Внутри resolve_urls​​​, если указан параметр url, выполните фильтрацию результатов базы данных для получения только URL-адресов, которые содержат указанное значение, используя метод Q(full_url__icontains=url).

Полное содержание файла shortener/schema.py показано здесь:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType
from django.db.models import Q

from .models import URL


class URLType(DjangoObjectType):
    class Meta:
        model = URL


class Query(graphene.ObjectType):
    urls = graphene.List(URLType, url=graphene.String())

    def resolve_urls(self, info, url=None, **kwargs):
        queryset = URL.objects.all()

        if url:
            _filter = Q(full_url__icontains=url)
            queryset = queryset.filter(_filter)

        return queryset


class CreateURL(graphene.Mutation):
    url = graphene.Field(URLType)

    class Arguments:
        full_url = graphene.String()

    def mutate(self, info, full_url)
        url = URL(full_url=full_url)
        url.save()

        return CreateURL(url=url)


class Mutation(graphene.ObjectType):
    create_url = CreateURL.Field()

Сохраните и закройте файл. Если ваш локальный сервер не запущен, запустите его с помощью команды python manage.py runserver.

Проверьте внесенные изменения, перейдя на страницу http://localhost:8000/graphql. В интерфейсе GraphiQL добавьте следующее выражение. Оно будет фильтровать все URL-адреса со словом community:

query {
  urls(url:"community") {
    id
    fullUrl
    urlHash
    clicks
    createdAt
  }
}

Вывод представляет собой одну запись, поскольку вы добавили один URL со строкой community. Если вы добавили несколько URL-адресов ранее, вывод может отличаться.

Output
{ "data": { "urls": [ { "id": "1", "fullUrl": "https://www.digitalocean.com/community", "urlHash": "077880af78", "clicks": 1, "createdAt": "2020-01-30T19:27:36.243900+00:00" } ] } }

Теперь у вас есть возможность выполнять поиск по вашим URL-адресам. Однако при большом количестве ссылок ваши клиенты могут жаловаться, что список URL-адресов возвращает больше данных, чем могут обработать их приложения. Чтобы устранить эту проблему, необходимо добавить пагинацию.

Шаг 8 — Реализация пагинации

Клиенты, использующие ваш сервер, могут жаловаться, что на получение ответа уходит очень много времени, либо на слишком большой размер, если он содержит слишком много URL. Даже ваша база данных может не справляться со сборкой большого набора информации. Чтобы решить эту проблему, вы можете разрешить клиенту указывать, сколько элементов он хочет получать в каждом запросе, используя технику под названием pagination.

Используемого по умолчанию способа для реализации этой функции не существует. Даже в REST API вы можете видеть ее в заголовках HTTP или параметрах запроса с разными именами и поведением.

В этом приложении мы будем применять пагинацию, добавив два аргумента в запрос URL: first и skip. first выполняет выбор первого переменного числа элементов, а skip будет указывать, сколько элементов следует пропустить с начала. Например, используя first == 10 и skip == 5, вы получите первые 10 URL-адресов, но пропустите 5 из них, возвращая только 5 оставшихся.

Реализация этого решения аналогична добавлению фильтра.

Откройте файл shortener/schema.py:

  1. vim shortener/schema.py

В файле измените класс Query, добавив два новых параметра в переменную urls и метод resolve_urls, как показано в следующем коде:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType
from django.db.models import Q

from .models import URL


class Query(graphene.ObjectType):
    urls = graphene.List(URLType, url=graphene.String(), first=graphene.Int(), skip=graphene.Int())

    def resolve_urls(self, info, url=None, first=None, skip=None, **kwargs):
        queryset = URL.objects.all()

        if url:
            _filter = Q(full_url__icontains=url)
            queryset = queryset.filter(_filter)

        if first:
            queryset = queryset[:first]

        if skip:
            queryset = queryset[skip:]

        return queryset
...

Этот код использует созданные параметры first и skip внутри метода resolve_urls для фильтрации запроса базы данных.

Сохраните и закройте файл. Если ваш локальный сервер не запущен, запустите его с помощью команды python manage.py runserver.

Чтобы протестировать пагинацию, воспользуйтесь следующим запросом в интерфейсе GraphiQL на странице http://localhost:8000/graphql:

query {
  urls(first: 2, skip: 1) {
    id
    fullUrl
    urlHash
    clicks
    createdAt
  }
}

Ваш инструмент для сокращения URL-адресов будет возвращать второй URL, созданный в базе данных:

Output
{ "data": { "urls": [ { "id": "2", "fullUrl": "https://www.digitalocean.com/write-for-donations/", "urlHash": "703562669b", "clicks": 0, "createdAt": "2020-01-30T19:31:10.820062+00:00" } ] } }

Это показывает, что функция пагинации работает. Вы можете попробовать добавить несколько URL-адресов и протестировать разные значения first и skip.

Заключение

Экосистема GraphQL развивается каждый день и поддерживается активным сообществом. Она доказала свою способность для производства и используется такими компаниями, как GitHub и Facebook. И теперь вы можете использовать эту технологию для ваших проектов.

В этом обучающем руководстве вы создали сервис для сокращения URL-адресов с помощью GraphQL, Python и Django, используя такие понятия, как запросы и мутации. Но помимо этого, теперь у вас есть понимание того, как использовать эти технологии для создания веб-приложений, используя веб-фреймворк Django.

Вы можете узнать больше о GraphQL и инструментах, используемых здесь, на сайте GraphQL и сайтах с документацией для Graphene. DigitalOcean предоставляет дополнительные руководства для Python и Django, если вы хотите узнать больше.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors

Default avatar

Senior Technical Editor

Editor at DigitalOcean, fiction writer and podcaster elsewhere, always searching for the next good nautical pun!


Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel