Django REST Framework 是什么

如果你打算使用 Django 搭建一个 RESTful API 后端,你完全有必要学习 Django REST Framework。

Django REST Framework 提供了 Serializers、APIView、GeneticAPIView、ViewSets、权限管理、搜索、分页等功能。这些功能、特性可以全部加入我们的 RESTful 后端,也可以选一部分加入。

如果你和我一样,第一次接触后端,尚不了解 RESTful 后端的组成、功能,可能也会对上面的这些概念懵圈。

用通俗的话来说,RESTful 后端开发过程中,包含了相当多的重复元素,比如:

  1. 将数据模型(Django 中特指我们编写的 models.Model 类)变为 json 字符串发送给前端,这个变的过程称为序列化
  2. RESTful API 应该对 GET /activities/ 这类请求提供获取活动列表的功能,对 POST /activities/ 提供创建活动的功能;
  3. 对于获取活动列表的功能,还应当具有搜索和分页的功能;
  4. RESTful API 应该对 GET /activities/1/ 这类请求提供获取 id 为 1 的活动信息的功能,对 PUT /activities/1/PATCH /activities/1/ 提供修改 id 为 1 的活动信息的功能,对 DELETE /activities/1/ 提供删除 id 为 1 的活动的功能
  5. 当然,还需要判定用户是否有权限获取、修改

上述步骤,使用纯 Django 也可以完成。而 Django REST Framework 所完成的,就是将具体的、重复的、步骤抽象化、简短化:将 json 到数据模型抽象化为序列器 Serializer 类,借助 Serializer 类编写 序列化 的过程可以最短缩减到三行;将提供了通用 RESTful 功能(如提供列表、创建对象、修改、删除)的一些 Django 视图 View 抽象化为 GeneticAPIView,读者在后面可以看到,对不同的 HTTP 方法提供不同功能的代码借助 GeneticAPIView 可以最短缩减到三行,搜索分页加起来也可以不超过十行;甚至的甚至,相同操作的不同 Views(如,需要对 /activities//users/ 提供相同的列表、创建、获取、修改、删除方法)甚至可以用一个 ViewSet 进行描述。

当然,这种方法适合描述 RESTful API 中大部分通用功能,如果涉及到更细化的操作,如 signuplogin,就没法用到 ViewSets 这类操作了。而在过于抽象的代码上,如果需求发生了少许更改,也可能导致较大的代码改动,如对于 /activities//users/,原本二者提供的功能相同,使用一个 ViewSet 就可以描述,突然要求对 /users/ 中的注册过程添加一个验证码,都会导致这部分的代码重写。

综上所属,抽象化有其优点也有其缺点,在实际编码过程中,并不一定使用到 ViewSets,我的项目中最抽象也只用到了 GeneticAPIView

Django 入门

Django 入门

Django REST Framework 教程中也包含了必要的 Django 知识,不过我仍然建议简单看一下 Django 入门教程;以及,很多时候也会去查 Django 的文档。

Django REST Framework 官方教程

汉化版教程链接

自定义 APIException

Django 中的 raise Http404 用着很爽,可以在任何函数打断当前 view 的执行,直接返回一个 404 的 HttpResponse。但是不能自己定义别的返回值,诸如 104、500、503 等等。

一种思路是手写一个装饰器,在原本的 view 外包装一个 try ... catch 代码块,再自己定义各种 Exception 就可以实现了。

不过,Django REST Framework 在自己的 View 中提供了这个功能,我们只需要继承 rest_framework.exceptions.APIException 就可以了。

1
2
3
4
5
6
class OnedriveUnavailableException(APIException):
status_code = 503
default_detail = 'Onedrive 服务未登录。'


# raise OnedriveUnavailableException 即可返回 503,{"detail": "Onedrive 服务未登录。"}

太香了!

例:自定义对象级权限

文档

需求是这样的,我们需要仿照 IsAdminOrReadOnly,编写一个 IsPresenterOrAdminOrReadOnly

1
2
3
4
5
6
7
class IsPresenterOrAdminOrReadOnly(BasePermission):
def has_object_permission(self, request, view, activity: Activity):
return bool(
request.method in SAFE_METHODS or
request.user and
(activity.presenter.filter(id=request.user.id) or request.user.is_staff or request.user.is_superuser)
)
1
2
3
4
5
6
class ActivityAttenderUpdateView(GenericAPIView):
permission_classes = (IsPresenterOrAdminOrReadOnly,)

def patch(self, request: WSGIRequest, id: int) -> Response:
activity = get_object_or_404(Activity, id=id)
self.check_object_permissions(request, activity)

如果你正在编写自己的视图并希望强制执行对象级权限检测,或者你想在通用视图中重写 get_object 方法,那么你需要在检索对象的时候显式调用视图上的 .check_object_permissions(request, obj) 方法。

例:在序列化后修改字段

DRF 的 GeneticAPIView 写起来真的很爽,把 Models 和 Serializer 写好以后,GeneticAPIView 只需要几行就能写完,完成搜索、分页、序列化、返回 Response。但是,不可避免的是,某些时候想要传回更多的字段,或者由于权限问题隐藏某些字段。这个时候,我们就需要自己改写一部分 GeneticAPIView。

在 view 中的 serializer_data 中添加字段

需求是这样的:原本的 login 函数完成了接收 usernamepassword 并验证,正确后将该用户的信息用 UserSerializer 序列化后返回:

1
2
3
4
5
6
7
8
9
10
11
12
def login(request: WSGIRequest) -> Response:
err_response = Response(status=status.HTTP_401_UNAUTHORIZED)
if 'username' not in request.data or 'password' not in request.data:
return err_response
username = request.data['username']
password = request.data['password']
user = authenticate(request, username=username, password=password)
if user is None:
return err_response
django_login(request, user)
serializer = UserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK)

由于某些原因,我需要把 csrftoken 加入到返回的 JSON 中。csrftoken 的获取方法是 csrf.get_token(request)。如何将 csrftoken 加入序列化字段呢?

最容易想到,但最麻烦且最不美观的方法是修改 UserSerializer。有没有其他方法呢?

在最后一行打断点然后调试,执行到这里时看一眼 serializer.data 的类型,是 OrderedDictOrderedDict 就很好办了,在 Response 之前修改一下,添加一个字段就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
def login(request: WSGIRequest) -> Response:
err_response = Response(status=status.HTTP_401_UNAUTHORIZED)
if 'username' not in request.data or 'password' not in request.data:
return err_response
username = request.data['username']
password = request.data['password']
user = authenticate(request, username=username, password=password)
if user is None:
return err_response
serializer = UserSerializer(user)
serializer_data = serializer.data
serializer_data['csrftoken'] = csrf.get_token(request)
return Response(serializer_data, status=status.HTTP_200_OK)

在 GeneticAPIView 的 serializer_data 中修改字段

这个看起来就要麻烦一点了,毕竟本身 GeneticAPIView 部分是一个函数都没有写。

1
2
3
4
5
6
class UserListView(ListAPIView):
queryset = User.objects.all().order_by("-userprofile__experience")
filter_backends = (filters.SearchFilter,)
search_fields = ('username', 'first_name', 'last_name', 'userprofile__student_id')
pagination_class = Pagination
serializer_class = UserSerializer

这里的需求是:为了保护用户隐私,对于非管理员用户,不让其获取用户的 usernamestudent_id,将返回的 username 改为 ***student_id 改为前四位(代表入学年份)。

这里就有两种思路了:修改每一个 View 的处理过程(不仅仅是这个 View 需要保护隐私,其他 View 也应当保护隐私);或者直接修改 Serializer 的序列化过程。

思路 1:修改 View 的过程

我们可以考虑类似于上面添加 csrftoken 字段的思路。读 ListAPIView 的源码后,可以知道,get 方法调用了 list 方法,而 list 中做了查询、分页、序列化、包装成分页形式再返回的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ListAPIView(mixins.ListModelMixin,
GenericAPIView):
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

class ListModelMixin:
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())

page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

我们只需要把官方的 list 方法复制下来,然后改写一下,在序列化以后判断是否是管理员,对于非管理员,替换每个 usernamestudent_id 就可以了。

注意到官方给的 list 用 if 判断了是否用到分页功能,针对不同情况作了处理。我们已经用到了分页,所以可以把 if 删掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UserListView(ListAPIView):
queryset = User.objects.all().order_by("-userprofile__experience")
filter_backends = (filters.SearchFilter,)
search_fields = ('username', 'first_name', 'last_name', 'userprofile__student_id')
pagination_class = Pagination
serializer_class = UserSerializer

# 复制官方的 list,然后根据自己的需求进行改写
def list(self, request: WSGIRequest, *args, **kwargs) -> Response:
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page, many=True)
serializer_data = serializer.data
# 对于非管理员,需要替换学号为学号前四位
if not (request.user.is_staff or request.user.is_staff):
for item in serializer_data:
item['username'] = '***'
item['student_id'] = item['student_id'][0:4]
return self.get_paginated_response(serializer_data)

这种方法的缺点就是需要对每一个 View 进行修改,而且在 List 的结果中,非管理员也不能获得自己的详细信息了。

思路 2:修改 Serializer

上面的方法需要对每个 View 进行修改,并且新增的 View 如果忘记修改了,还会导致数据泄露。有没有从 Model 或 Serializer 下手的方法呢?

这种方案要解决两个问题,一是序列化的过程中并没有 request,没有 request 我们就没法判断当前用户是否是管理员,所以需要通过什么方法传进去;二是需要自定义序列化的过程。


对于第一个问题,DRF 文档 中提到,可以在序列化时提供 context

1
2
3
serializer = AccountSerializer(account, context={'request': request})
serializer.data
# {'id': 6, 'owner': 'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'}

context 中提供 user 信息,我们就可以判断用户的身份了。更好的是,GeneticAPIView 在序列化时,提供了默认的 context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""

def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)

def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}

这下我们直接在 Serializer 里用就行了,GeneticAPIView 一行都不用修改。


对于第二个问题,查询资料发现,DRF 序列化的函数为 to_representation,其定义如下:

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
class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
ret = OrderedDict()
fields = self._readable_fields

for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue

# We skip `to_representation` for `None` values so that fields do
# not have to explicitly deal with that case.
#
# For related fields with `use_pk_only_optimization` we need to
# resolve the pk value.
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)

return ret

我们只需要重写它即可,把上面的代码复制下来,然后在 return 之前判断用户身份,替换 retstudent_idusername 字段。

不对,连复制都不需要,我们只需要调用父类的 to_representation(),然后加两行就可以了!

1
2
3
4
5
6
7
8
9
10
11
def to_representation(self, instance):
ret = super().to_representation(instance)

# 隐藏 username 和 student_id
request = self.context.get('request')
if request is None: # django shell 时会出现 request is None
return ret
if not (request.user.is_staff or request.user.is_superuser or request.user.id == ret['id']):
ret['username'] = '***'
ret['student_id'] = ret['student_id'][0:4]
return ret

REST API 标准

开发 REST 的工具有了,那标准呢?这是一个非常重要的问题。就像学了 C 语言后能写出很多的程序,但是常用的代码风格、代码库依旧要参考其他的标准。

我为此新开了一篇博文:RESTful API 标准

DRF 项目部署

可以参考 https://github.com/uestc-msc/uestcmsc_webapp_backend/blob/lyh543/docs/deploy/deploy.md

Swagger 文档生成

可以看 drf-yasg