记录 DRF 学习片段
chenzuoqing Lv3

记录 DRF 学习片段

刷官方 tutorial 的记录,官网地址 django-rest-framework

开始

代码不是全部都贴出,仅做记录(不难看懂)

参考 q1mi老师翻译的文档

示例 models.py 中的 Snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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',)

示例的序列化类 serializers.py 定义,写法的进化过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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

# 使用封装的序列化器
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')

序列化和反序列化

基础的序列化过程,Snippet 是个表 model,以及它的序列化类 SnippetSerializer,先忽略他们定义的内容,看序列化做的事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) # 将model对象转成python原生数据类型
print(serializer.data) # 字典数据,{'id': 1, ...}

# SnippetSerializer(Snippet.objects.all(), many=True) # 不是单个对象而是querySet时,传many=True,可以序列化多个

content = JSONRenderer().render(serializer.data) # 将数据转成JSON
print(content) # b'{"id": 2, ...}'

反序列化过程,类似 forms 验证,接上以上代码

1
2
3
4
5
6
7
8
import io

stream = io.BytesIO(content)
data = JSONParser().parse(stream) # 接受JSON数据,模拟从请求中读取的数据
serializer = SnippetSerializer(data=data) # 传入反序列化的数据
serializer.is_valid() # 校验数据是否缺少
serializer.validated_data # 校验字段内容
serializer.save() # 保存对象

序列化类

介绍 rest_framework.serializers 中创建序列化器的快捷方式

ModelSerializer

类似 django forms 中的 ModelForm

  • 可简单定义需要序列化的字段
  • 默认实现的 createupdate 方法,用于增加和更新
1
2
3
4
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')

HyperlinkedModelSerializer

可以更好的处理对象实体之间的关系

这个序列化器在有关联字段的时候,展示的不是 ID,而是相关对象的 URL,与 ModelSerializer 有区别

  • 默认不包括 ID 字段
  • 数据含一个url字段,使用HyperlinkedIdentityField
  • 使用的关联使用 HyperlinkedRelatedField 而不是 PrimaryKeyRelatedField (它会把关联的对象仅给出 ID 返回)

PrimaryKeyRelatedField

得到此对象对应的多个关联对象ID,如解析 User 实例的 Snippet 对象:

1
2
3
4
5
6
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
# 返回如: {"id":1, "snippets": [1,2,3], ...}
class Meta:
model = User
fields = ('id', 'username', 'snippets')

HyperlinkedRelatedField

得到此对象对应的多个关联对象实体 URL(需要传入 view_name

如下,解析 User 实例的 Snippet 对象,snippet-detail 对应 urls.py 中的 name 参数(参考后面关系和超链接部分)

1
2
3
4
5
6
7
class UserSerializer(serializers.HyperlinkedModelSerializer):
snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)
# 返回如: {"id":1, "snippets": ["http://127.0.0.1:8000/snippets/1/", ...], ...}

class Meta:
model = User
fields = ('id', 'username', 'snippets', 'url')

请求和响应

请求对象

1
2
request.POST  # 只试用与POST方法
request.data # 处理任意数据 适用于'POST','PUT'和'PATCH'方法

响应对象

继承了 django 中的 SimpleTemplateResponse ,但未渲染模板,会使用内容协商来确定返回给客户端的正确内容类型

1
2
# from rest_framework.response import Response
return Response(data)

状态码

命名了 HTTP 状态码,便于使用

1
2
# from rest_framework import status
return Response(data, status=status.HTTP_201_CREATED)

API 视图包装器

均可限制访问接口的方法,否则抛出 405 Method Not Allowed 错误

  • APIView 供类视图继承,与 django 的类视图相似,路由中传入 as_view,并内置dispatch方法,若请求方法未实现将抛出 405
  • api_view 给函数视图使用的装饰器,默认仅允许 GET 请求,可传入 method 列表
  • action 给视图集增加动作,如一个 viewSet 默认可能有detaillist 等方法,若它注册路由时设置 basename=snippets 将自动生成可解析的 snippets-detail 等动作。action可以增加自定义的动作,如下有增加 snippets-highlight 的示例,默认后缀为被装饰的方法名(viewSet 路由注册部分看 viewsets 视图集和 routers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from rest_framework.views import APIView
from rest_framework.decorators import api_view, action


class Demo(APIView):
def get(self, request, format=None):
pass

def post(self, request, format=None):
pass


@api_view(['GET', 'POST'])
def api_func_demo(request)
if request.method == "GET":
pass
elif request.method == "POST":
pass


class SnippetViewSet(viewsets.ModelViewSet):
"""
Snippet的常规增删查改、列表视图
还提供了一个额外的`highlight`操作
"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)

# 注意action装饰的方法替代了上面的SnippetHighlight
# 默认仅允许GET方法,methods指定
# detail表示是否适用于实例详细信息,需要实例参数pk=1这种情况
# 使用Router且basename=snippet时,此动作解析为'snippet-highlight',与序列化中的view_name对应
@action(renderer_classes=[renderers.StaticHTMLRenderer], detail=True, methods=['get'])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)

def perform_create(self, serializer):
serializer.save(owner=self.request.user)

类视图

mixins 混合类

rest_framework.mixins 中,定义了很多可服用的行为,对应我们接口处理 model 的查询、修改、序列化、反序列化、保存等操作,它们需要和 generics.GenericAPIView 一起继承,搭配使用

  • CreateModelMixin 用于创建对象,封装了 create ,调用序列化类校验数据、保存等操作
  • ListModelMixin 用于列出多个对象,封装了 list 方法,从 model 中查询数据,并序列化成数据、分页返回给请求
  • RetrieveModelMixin 用于查找单个对象示例,封装了 retrieve 方法,返回单个对象已序列化的数据
  • UpdateModelMixin 用于更新数据,封装了 update 方法,更新单个对象的数据
  • DestroyModelMixin 用于删除对象,封装了 destroy 方法,查找对象实例进行删除

继承了上面多个类将有多个类的功能,如下将 mixin 中的封装对应 HTTP 方法的处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics


class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)

generics 通用类

rest_framework.generics 中有已经内置混合好的通用视图,用它可以将HTTP方法与 Mixin 封装对应上。

  • GenericAPIView 是通用视图中最基础的类,继承 views.APIView,带有序列化、分页、对象过滤等封装

使用通用类视图写接口,代码量将变的很少,逻辑就像配置一样,设置 queryset序列化类就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import generics


class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer

基于上面两个视图,修改 urls.py,增加 URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.contrib import admin
from django.urls import path
from django.conf.urls import include
from snippets import views

urlpatterns = [
path('admin/', admin.site.urls),
# 方便调试接口,需要注意namespace
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),

# 注意path的传参,参数名为pk,类型int
path('snippets/', views.SnippetList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]

运行测试(创建用户访问 http://localhost:8000/api-auth/login 再打开 /snippets/ 页面)

  • /snippets/ 列表数据

    image

  • /snippets/1/ 可以查看id=1的对象,还显示了其他可操作的方法

    image

viewsets 视图集和 routers

应该是最常用的方式,包括这种方式的路由

ViewSetView 很类似,但它提供的是读取、更新、创建、删除的操作,而不是类里面 getpost 这种请求处理的函数。ViewSet 类可以不自己写 URL,使用 rest_framework.routers 中的路由类,可以自动生成 URL

内置了几个常用类,继承自 mixingenerics 组成了几种常用的搭配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 提供as_view方法
# 将GET、POST、PUT、DELETE请求映射到了子类的list|retrieve、create、update|partial_update、destory等方法
class ViewSetMixin

# 不包含任何操作
class ViewSet(ViewSetMixin, views.APIView)

# 包含GenericAPIView功能的通用视图集
class GenericViewSet(ViewSetMixin, generics.GenericAPIView)

# 只读方法,可查看多个和单个实例数据
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet)

# 包含CRUD方法
class ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet)

使用示例,由上面的函数签名可以知道,ReadOnlyModelViewSet 只有读的方法

1
2
3
4
5
6
7
8
from rest_framework import viewsets

class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
此视图自动提供`list`和`detail`操作。
"""
queryset = User.objects.all()
serializer_class = UserSerializer

增加路由的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.contrib import admin
from django.urls import path
from django.conf.urls import include
from rest_framework.routers import DefaultRouter
from snippets import views

# 将根据basename生成解析,默认反解的api-root是接口目录路径
router = DefaultRouter()
# 各个方法将解析为类似'snippet-detail'等,@action装饰的动作如'snippet-方法名'
router.register('users', views.UserViewSet, basename='snippet')
router.register('snippets', views.SnippetViewSet, basename='user')

# 将router2的路由扩展到router中
router2 = DefaultRouter()
router.registry.extend(router.registry)

urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('', include(router.urls)),
]

权限

内置权限类

rest_framework.permissions 中有权限验证相关的内容

通常权限继承自 BasePermission 基类,主要定义了两个方法,均返回 Bool 值,用于区分是否有权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 权限的基类,定义了 has_permission 和 has_object_permission 两个方法
class BasePermission(metaclass=BasePermissionMetaclass):
"""
A base class from which all permission classes should inherit.
"""

def has_permission(self, request, view): # 视图级别权限
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True

def has_object_permission(self, request, view, obj): # 对象级别权限,多传递了一个对象参数
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True

自定义权限只需要继承 BasePermission 在两个方法中做判断,然后在视图中指定即可。

rest_framework.permissions 中内置了以下几种权限:

  • IsAuthenticated
  • IsAdminUser
  • IsAuthenticatedOrReadOnly
  • DjangoModelPermissions
  • DjangoModelPermissionsOrAnonReadOnly
  • DjangoObjectPermissions

自定义权限

例如在 snippets model 增加了 owner 字段关联用户,并且增加 permissions.py,实现只有 owner 用户可以修改自己的 snippets,其他情况均只读的权限类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
"""
自定义权限只允许对象的所有者编辑它。
"""
def has_object_permission(self, request, view, obj):
# 读取权限允许任何请求,
# 所以我们总是允许GET,HEAD或OPTIONS请求。
if request.method in permissions.SAFE_METHODS:
return True

# 只有该snippet的所有者才允许写权限。
return obj.owner == request.user

修改视图,导入新增的权限类并设置,这将只有 owner 可以修改

1
2
3
4
5
6
from snippets.permissions import IsOwnerOrReadOnly

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)

关系和超链接

为 API 根路径设置一个入口,snippets/views.py 中增加一个函数视图,使用 reverse 返回完全限定的 URL

1
2
3
4
5
6
7
8
9
10
11
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse


@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})

修改 urls.py 给 URL 加上 name,用于 reverse 解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.contrib import admin
from django.urls import path
from django.conf.urls import include
from snippets import views

urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),

path('snippets/', views.SnippetList.as_view(), name='snippet-list'),
path('snippets/<int:pk>/', views.SnippetDetail.as_view(), name='snippet-detail'),
path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view(), name='snippet-highlight'),
path('users/', views.UserList.as_view(), name='user-list'),
path('users/<int:pk>/', views.UserDetail.as_view(), name='user-detail'),

path('', views.api_root)
]
  • 此时访问接口根路径可以得到接口列表

    image

  • 访问连接得到实体 URL 链接(代码未贴出,参考项目)

    image

 Comments