September 4, 2019

DRF - Сериализатор

В этом руководстве будет рассказано о создании простого сервиса для вставки кода в веб-API.

Мы познакомимся в различными компонентамми, составляющими структуру REST. Вы получите широкое представление о том, как все они сочетаются друг с другом.

Этот урок достаточно объемный и более углублёный, чем первый. Поэтому, прежде чем начать, вам стоит прочитать руководство по быстрому старту.

Настройка новой виртуальной среды

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

virtualenv -p python3 venv
source ./venv/bin/activate

Теперь, когды наша виртуальная среда активирована, установим необходимые пакеты:

pip install django
pip install djangorestframework
pip install pygments  # Мы будем использовать этот пакет для подсветки кода

Начало работы

Мы почти готовы к разработке. Давайте создадим новый проект и приложение, которое будем использовать для создания простого веб-API.

django-admin startproject serializer-tutorial
cd serializer-tutorial
python manage.py startapp snippets

Нам нужно добавить наше новое приложение snippets и приложение rest_framework в INSTALLED_APPS.

Давайте отредактируем файл serializer-tutorial/settings.py:

INSTALLED_APPS = [
    ...
    'rest_framework',
    'snippets.apps.SnippetsConfig',
]

Теперь мы готовы к работе.

Создание модели

В данном руководстве мы начнем с создания простой модели Snippet, которая используется для хранения фрагментов кода. Отредактируйте файл snippets/models.py.

from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)

    class Meta:
        ordering = ['created']

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

python manage.py makemigrations snippets
python manage.py migrate

Создание сериализатора

Первое, что нам нужно для начала работы с нашим API - это предоставить способ сериализации и десериализации snippets в представления, такие как json. Мы можем сделать это, объявив сериализаторы, которые работают очень похоже на form в Django.

Создайте файл в snippets с именем serializers.py и добавьте следующее:

from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(required=False, allow_blank=True, max_length=100)
    code = serializers.CharField(style={'base_template': 'textarea.html'})
    linenos = serializers.BooleanField(required=False)
    language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
    style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')

    def create(self, validated_data):
        """
        Создает и возвращает объкт класса `Snippet`, когда данные прошли валидацию.
        """
        return Snippet.objects.create(**validated_data)

    def update(self, instance, validated_data):
        """
        Изменяет существующий объект класса `Snippet`, когда данные прошли валидацию.
        """
        instance.title = validated_data.get('title', instance.title)
        instance.code = validated_data.get('code', instance.code)
        instance.linenos = validated_data.get('linenos', instance.linenos)
        instance.language = validated_data.get('language', instance.language)
        instance.style = validated_data.get('style', instance.style)
        instance.save()
        return instance

Первая часть класса сериализатора определяет поля, которые сериализуются / десериализуются. Методы create () и update () определяют, как экземпляры класса создаются или изменяются при вызове serializer.save ()

Класс сериализатора очень похож на класс Form в Django и включает в себя похожие проверки в различных полях, такие как required, max_length и default.

Флаги полей также могут управлять отображением сериализатора при определенных обстоятельствах, например при рендеринге в HTML. Указанный выше флаг {'base_template': 'textarea.html'} эквивалентен использованию widget = widgets.Textarea в классе Django Form.

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

Работа с сериализатором

Прежде чем мы продолжим, мы ознакомимся с использованием нашего нового класса Serializer. Давайте заглянем в оболочку Django.

python manage.py shell

Сначала импортируем всё необходимое, затем создадим пару snippet`ов:

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser

snippet = Snippet(code='foo = "bar"\n')
snippet.save()

snippet = Snippet(code='print("hello, world")\n')
snippet.save()

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

serializer = SnippetSerializer(snippet)
serializer.data
# {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}

На данный момент мы перевели экземпляр модели в нативные типы данных Python. Чтобы завершить процесс сериализации, мы рендерим данные в json.

content = JSONRenderer().render(serializer.data)
content
# b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}'

Десериализация происходит аналогично. Сначала мы преобразуем поток в нативные типы данных Python.

import io

stream = io.BytesIO(content)
data = JSONParser().parse(stream)

Затем мы восстанавливаем эти нативные типы данных в полностью заполненный экземпляр объекта.

serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()
# <Snippet: Snippet object>

Обратите внимание, насколько похож API на работу с формами. Сходство должно стать еще более очевидным, когда мы начнем писать views, использующие наш сериализатор.

Мы также можем сериализовать queryset вместо экземпляров модели. Для этого мы просто добавляем флаг many = True к аргументам сериализатора.

serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data
# [OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]

Использовалие ModelSerializer

Наш класс SnippetSerializer копирует много информации, которая также содержится в модели Snippet. Было бы хорошо, если бы мы могли сделать наш код немного более кратким.

Так же, как Django предоставляет и классы Form, и классы ModelForm, инфраструктура REST включает в себя как классы Serializer, так и классы ModelSerializer.

Давайте посмотрим на рефакторинг нашего сериализатора с использованием класса ModelSerializer. Снова откройте файл snippets/serializers.py и замените класс SnippetSerializer следующим.

class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style']

Хорошим свойством сериализаторов является то, что вы можете проверить все поля в экземпляре сериализатора, напечатав его представление. Откройте оболочку Django с помощью python manage.py shell, затем попробуйте следующее:

from snippets.serializers import SnippetSerializer
serializer = SnippetSerializer()
print(repr(serializer))
# SnippetSerializer():
#    id = IntegerField(label='ID', read_only=True)
#    title = CharField(allow_blank=True, max_length=100, required=False)
#    code = CharField(style={'base_template': 'textarea.html'})
#    linenos = BooleanField(required=False)
#    language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
#    style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...

Важно помнить, что классы ModelSerializer не делают ничего волшебного, они просто сокращение для создания классов сериализатора:

  • Автоматически определяют набор полей.
  • Реализуют по умолчанию методы create () и update ().

Создание Django views с использованием сериализатора

Давайте посмотрим, как мы можем написать views API, используя наш новый класс Serializer. На данный момент мы не будем использовать другие функции REST Framework, мы просто напишем views как обычно в Django.

Отредактируйте файл snippets/views.py и добавьте следующее.

from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer

Корнем нашего API будет views, которое поддерживает показ всех существующих snippet или создание нового snippet.

@csrf_exempt
def snippet_list(request):
    """
    Показать все сниппеты или создать новый.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JsonResponse(serializer.data, safe=False)

    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)

Обратите внимание, что, поскольку мы хотим иметь возможность отправлять POST в это view от клиентов, которые не имеют CSRF токена, нам необходимо пометить view декоратором csrf_exempt.

Views инфраструктуры REST на самом деле используют более разумное решение, чем это, но это подойдет для наших целей на данный момент.

Нам также потребуется view, которое соответствует snippet фрагменту и может использоваться для извлечения, обновления или удаления фрагмента.

@csrf_exempt
def snippet_detail(request, pk):
    """
    Retrieve, update or delete a code snippet.
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return JsonResponse(serializer.data)

    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(snippet, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)

    elif request.method == 'DELETE':
        snippet.delete()
        return HttpResponse(status=204)

Наконец нам нужно связать эти view.

Создайте файл snippets/urls.py:

from django.urls import path
from snippets import views

urlpatterns = [
    path('snippets/', views.snippet_list),
    path('snippets/<int:pk>/', views.snippet_detail),
]

В файле serializer-tutorial/urls.py, чтобы включить URL-адреса нашего приложения, добавьте:

from django.urls import path, include

urlpatterns = [
    path('', include('snippets.urls')),
]

Стоит отметить, что есть пара узких мест, с которыми мы сейчас не имеем дело.

Если мы отправим искаженный json или если запрос сделан с помощью метода, который не обрабатывается представлением, мы получим ответ 500 «ошибка сервера».

Тестирование нашего веб-API

Теперь мы можем запустить сервер, который обслуживает наши snippets.

Выйти из оболочки ...

quit()

И запустить сервер разработки Django.

python manage.py runserver

Validating models...

0 errors found
Django version 1.11, using settings 'tutorial.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

В другом окне терминала мы можем протестировать сервер. Мы можем протестировать наш API используя curl или httpie.

Httpie - это удобный http-клиент, написанный на Python.

Давайте установим это. Вы можете установить httpie используя pip:

pip install httpie

Наконец, мы можем получить список всех сниппетов:

http http://127.0.0.1:8000/snippets/

HTTP/1.1 200 OK
...
[
  {
    "id": 1,
    "title": "",
    "code": "foo = \"bar\"\n",
    "linenos": false,
    "language": "python",
    "style": "friendly"
  },
  {
    "id": 2,
    "title": "",
    "code": "print(\"hello, world\")\n",
    "linenos": false,
    "language": "python",
    "style": "friendly"
  }
]

Или мы можем получить конкретный сниппет, ссылаясь на его ID:

http http://127.0.0.1:8000/snippets/2/

HTTP/1.1 200 OK
...
{
  "id": 2,
  "title": "",
  "code": "print(\"hello, world\")\n",
  "linenos": false,
  "language": "python",
  "style": "friendly"
}

Точно так же вы можете отобразить тот же самый json, посетив эти URL в веб-браузере.

Итог

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

В настоящее время наши представления API не делают ничего особенного, кроме обработки ответов json, и есть некоторые узкие места, в которых могут возникать ошибки.

Мы увидим, как мы можем начать улучшать наш проект далее.