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, и есть некоторые узкие места, в которых могут возникать ошибки.
Мы увидим, как мы можем начать улучшать наш проект далее.