教程 1:序列化

简介

本教程将介绍如何创建简单的粘贴代码高亮显示 Web API。在此过程中,它将介绍构成 REST 框架的各个组件,并让你全面了解所有内容的关联方式。

本教程相当深入,因此在开始之前,你可能应该拿块饼干和一杯你最喜欢的饮品。如果你只想快速了解一下,你应该转到 快速入门 文档。


注意:本教程的代码可在 GitHub 上的 encode/rest-framework-tutorial 存储库中找到。已完成的实现也作为沙箱版本在线进行测试,可在此处获得


设置新环境

在执行任何其他操作之前,我们将使用 venv 创建一个新的虚拟环境。这将确保我们的软件包配置与我们正在处理的任何其他项目保持良好的隔离。

python3 -m venv env
source env/bin/activate

现在我们处于虚拟环境中,我们可以安装我们的软件包要求。

pip install django
pip install djangorestframework
pip install pygments  # We'll be using this for the code highlighting

注意:要随时退出虚拟环境,只需键入 deactivate。有关更多信息,请参阅 venv 文档

开始

好的,我们准备开始编码。首先,让我们创建一个新的项目来使用。

cd ~
django-admin startproject tutorial
cd tutorial

完成后,我们可以创建一个应用程序,我们将使用该应用程序创建一个简单的 Web API。

python manage.py startapp snippets

我们需要将我们的新 snippets 应用程序和 rest_framework 应用程序添加到 INSTALLED_APPS。让我们编辑 tutorial/settings.py 文件

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

好的,我们准备开始了。

创建要使用的模型

出于本教程的目的,我们将首先创建一个简单的 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 snippets

创建序列化器类

在 Web API 中开始操作的第一件事是提供一种将代码片段实例序列化和反序列化为诸如 json 的表示形式的方法。我们可以通过声明与 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):
        """
        Create and return a new `Snippet` instance, given the validated data.
        """
        return Snippet.objects.create(**validated_data)

    def update(self, instance, validated_data):
        """
        Update and return an existing `Snippet` instance, given the validated data.
        """
        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() 时如何创建或修改完全成熟的实例

序列化器类与 Django Form 类非常相似,并且在各种字段上包含类似的验证标志,例如 requiredmax_lengthdefault

字段标志还可以控制序列化器在某些情况下应如何显示,例如呈现为 HTML 时。上面的 {'base_template': 'textarea.html'} 标志等效于在 Django Form 类上使用 widget=widgets.Textarea。这对于控制可浏览 API 的显示方式特别有用,正如我们将在本教程后面看到的那样。

实际上,我们还可以通过使用 ModelSerializer 类来节省一些时间,正如我们将在后面看到的那样,但现在我们将保持序列化器定义明确。

使用序列化器

在我们继续之前,我们将熟悉使用我们的新序列化器类。让我们进入 Django shell。

python manage.py shell

好的,一旦我们完成了一些导入,让我们创建一些代码片段以供使用。

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()

我们现在有一些代码片段实例可以玩了。让我们看看如何序列化其中一个实例。

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 与使用表单的工作方式非常相似。当我们开始编写使用序列化器的视图时,这种相似性应该变得更加明显。

我们还可以序列化查询集,而不是模型实例。要做到这一点,我们只需向序列化器参数添加 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']

序列化器的一个很好的特性是,你可以通过打印其表示形式来检查序列化器实例中的所有字段。使用 python manage.py shell 打开 Django 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 视图

让我们看看如何使用我们的新序列化器类编写一些 API 视图。目前,我们不会使用 REST 框架的其他任何功能,我们只将视图编写为常规的 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 根部将是一个视图,它支持列出所有现有的代码片段,或创建新的代码片段。

@csrf_exempt
def snippet_list(request):
    """
    List all code snippets, or create a new snippet.
    """
    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)

请注意,由于我们希望能够从没有 CSRF 令牌的客户端向此视图发布,因此我们需要将视图标记为 csrf_exempt。这并不是你通常想做的事情,REST 框架视图实际上使用比这更明智的行为,但它现在可以满足我们的目的。

我们还需要一个视图,它对应于单个代码片段,并且可用于检索、更新或删除代码片段。

@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)

最后,我们需要连接这些视图。创建 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),
]

我们还需要连接根 URL 配置文件,在 tutorial/urls.py 文件中,以包含我们的代码片段应用程序的 URL。

from django.urls import path, include

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

值得注意的是,我们目前没有正确处理一些极端情况。如果我们发送格式错误的 json,或者使用视图不处理的方法发出请求,那么最终我们会得到一个 500 “服务器错误”响应。不过,这现在可以了。

测试我们对 Web API 的首次尝试

现在,我们可以启动一个提供代码片段的示例服务器。

退出 shell...

quit()

...并启动 Django 的开发服务器。

python manage.py runserver

Validating models...

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

在另一个终端窗口中,我们可以测试服务器。

我们可以使用 curlhttpie 测试我们的 API。Httpie 是用 Python 编写的用户友好的 http 客户端。让我们安装它。

你可以使用 pip 安装 httpie

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"
}

同样,您可以在 Web 浏览器中访问这些 URL 来显示相同的 json。

我们现在在哪里

到目前为止,我们做得很好,我们已经拥有了一个与 Django 的 Forms API 非常相似的序列化 API,以及一些常规的 Django 视图。

除了提供 json 响应之外,我们的 API 视图目前并没有什么特别之处,并且还有一些错误处理边缘情况我们仍然希望清理,但它是一个可用的 Web API。

我们将在本教程的 第 2 部分 中了解如何开始改进。