Django REST Framework 是什么
如果你打算使用 Django 搭建一个 RESTful API 后端,你完全有必要学习 Django REST Framework。
Django REST Framework 提供了 Serializers、APIView、GeneticAPIView、ViewSets、权限管理、搜索、分页等功能。这些功能、特性可以全部加入我们的 RESTful 后端,也可以选一部分加入。
如果你和我一样,第一次接触后端,尚不了解 RESTful 后端的组成、功能,可能也会对上面的这些概念懵圈。
用通俗的话来说,RESTful 后端开发过程中,包含了相当多的重复元素,比如:
- 将数据模型(Django 中特指我们编写的 models.Model 类)变为 json 字符串发送给前端,这个变的过程称为
序列化
; - RESTful API 应该对
GET /activities/
这类请求提供获取活动列表的功能,对POST /activities/
提供创建活动的功能; - 对于获取活动列表的功能,还应当具有搜索和分页的功能;
- RESTful API 应该对
GET /activities/1/
这类请求提供获取 id 为 1 的活动信息的功能,对PUT /activities/1/
和PATCH /activities/1/
提供修改 id 为 1 的活动信息的功能,对DELETE /activities/1/
提供删除 id 为 1 的活动的功能 - 当然,还需要判定用户是否有权限获取、修改
上述步骤,使用纯 Django 也可以完成。而 Django REST Framework 所完成的,就是将具体的、重复的、步骤抽象化、简短化:将 json 到数据模型抽象化为序列器 Serializer
类,借助 Serializer
类编写 序列化
的过程可以最短缩减到三行;将提供了通用 RESTful 功能(如提供列表、创建对象、修改、删除)的一些 Django 视图 View
抽象化为 GeneticAPIView
,读者在后面可以看到,对不同的 HTTP 方法提供不同功能的代码借助 GeneticAPIView
可以最短缩减到三行,搜索分页加起来也可以不超过十行;甚至的甚至,相同操作的不同 Views(如,需要对 /activities/
和 /users/
提供相同的列表、创建、获取、修改、删除方法)甚至可以用一个 ViewSet
进行描述。
当然,这种方法适合描述 RESTful API 中大部分通用功能,如果涉及到更细化的操作,如 signup
和 login
,就没法用到 ViewSets 这类操作了。而在过于抽象的代码上,如果需求发生了少许更改,也可能导致较大的代码改动,如对于 /activities/
和 /users/
,原本二者提供的功能相同,使用一个 ViewSet
就可以描述,突然要求对 /users/
中的注册过程添加一个验证码,都会导致这部分的代码重写。
综上所属,抽象化有其优点也有其缺点,在实际编码过程中,并不一定使用到 ViewSets
,我的项目中最抽象也只用到了 GeneticAPIView
。
Django 入门
Django REST Framework 教程中也包含了必要的 Django 知识,不过我仍然建议简单看一下 Django 入门教程;以及,很多时候也会去查 Django 的文档。
Django REST Framework 官方教程
例:在序列器中定义可修改的外键 id
1 | class Activity(models.Model): |
Django REST Framework 处理外键 id 有点复杂。因为它的 update
默认是修改 id 值:如果你想修改 Acitivity 的 banner.id
为 4,DRF 的默认操作是把这个 Acitivity 外键现在指向的 banner 的 id 改为 4,而不是把 Acitivity 的外键重新指向 id 为 4 的 banner。
但是,请注意到,万能的 Django 为这个 Activity 提供了一个 banner_id
属性。在读取的时候,这个东西和 banner.id
的值是一样的,但在写入的时候:
- 修改
banner.id
的操作是将外键现在指向的 banner 的 id 改为 4,且修改为None
时会报错:不存在 ActivityPhoto
- 修改
banner_id
的操作是将外键重新指向 id 为 4 的 banner,且修改为None
表示设置这个外键为 null
于是,Serializer 里直接写 banner_id
就行了。
1 | class ActivitySerializer(serializers.ModelSerializer): |
Django 永远滴神!
例:自定义 APIException
Django 中的 raise Http404
用着很爽,可以在任何函数打断当前 view 的执行,直接返回一个 404 的 HttpResponse。但是不能自己定义别的返回值,诸如 104、500、503 等等。
一种思路是手写一个装饰器,在原本的 view 外包装一个 try ... catch
代码块,再自己定义各种 Exception 就可以实现了。
不过,Django REST Framework 在自己的 View 中提供了这个功能,我们只需要继承 rest_framework.exceptions.APIException
就可以了。
1 | class OnedriveUnavailableException(APIException): |
太香了!
例:自定义对象级权限
需求是这样的,我们需要仿照 IsAdminOrReadOnly
,编写一个 IsPresenterOrAdminOrReadOnly
。
1 | class IsPresenterOrAdminOrReadOnly(BasePermission): |
1 | class ActivityAttenderUpdateView(GenericAPIView): |
如果你正在编写自己的视图并希望强制执行对象级权限检测,或者你想在通用视图中重写
get_object
方法,那么你需要在检索对象的时候显式调用视图上的.check_object_permissions(request, obj)
方法。
例:完成序列化后再修改字段
DRF 的 GeneticAPIView 写起来真的很爽,把 Models 和 Serializer 写好以后,GeneticAPIView 只需要几行就能写完,完成搜索、分页、序列化、返回 Response。但是,不可避免的是,某些时候想要传回更多的字段,或者由于权限问题隐藏某些字段。这个时候,我们就需要自己改写一部分 GeneticAPIView。
在 view 中的 serializer_data 中添加字段
需求是这样的:原本的 login 函数完成了接收 username
和 password
并验证,正确后将该用户的信息用 UserSerializer
序列化后返回:
1 | def login(request: WSGIRequest) -> Response: |
由于某些原因,我需要把 csrftoken
加入到返回的 JSON 中。csrftoken
的获取方法是 csrf.get_token(request)
。如何将 csrftoken
加入序列化字段呢?
最容易想到,但最麻烦且最不美观的方法是修改 UserSerializer
。有没有其他方法呢?
在最后一行打断点然后调试,执行到这里时看一眼 serializer.data
的类型,是 OrderedDict
。OrderedDict
就很好办了,在 Response 之前修改一下,添加一个字段就可以了。
1 | def login(request: WSGIRequest) -> Response: |
在 GeneticAPIView 的 serializer_data 中修改字段
这个看起来就要麻烦一点了,毕竟本身 GeneticAPIView 部分是一个函数都没有写。
1 | class UserListView(ListAPIView): |
这里的需求是:为了保护用户隐私,对于非管理员用户,不让其获取用户的 username
和 student_id
,将返回的 username
改为 ***
、student_id
改为前四位(代表入学年份)。
这里就有两种思路了:修改每一个 View 的处理过程(不仅仅是这个 View 需要保护隐私,其他 View 也应当保护隐私);或者直接修改 Serializer
的序列化过程。
思路 1:修改 View 的过程
我们可以考虑类似于上面添加 csrftoken
字段的思路。读 ListAPIView
的源码后,可以知道,get
方法调用了 list
方法,而 list
中做了查询、分页、序列化、包装成分页形式再返回的操作。
1 | class ListAPIView(mixins.ListModelMixin, |
我们只需要把官方的 list 方法复制下来,然后改写一下,在序列化以后判断是否是管理员,对于非管理员,替换每个 username
和 student_id
就可以了。
注意到官方给的 list
用 if 判断了是否用到分页功能,针对不同情况作了处理。我们已经用到了分页,所以可以把 if 删掉。
1 | class UserListView(ListAPIView): |
这种方法的缺点就是需要对每一个 View 进行修改,而且在 List 的结果中,非管理员也不能获得自己的详细信息了。
思路 2:修改 Serializer
上面的方法需要对每个 View 进行修改,并且新增的 View 如果忘记修改了,还会导致数据泄露。有没有从 Model 或 Serializer 下手的方法呢?
这种方案要解决两个问题,一是序列化的过程中并没有 request
,没有 request
我们就没法判断当前用户是否是管理员,所以需要通过什么方法传进去;二是需要自定义序列化的过程。
对于第一个问题,DRF 文档 中提到,可以在序列化时提供 context
。
1 | serializer = AccountSerializer(account, context={'request': request}) |
在 context
中提供 user 信息,我们就可以判断用户的身份了。更好的是,GeneticAPIView 在序列化时,提供了默认的 context
:
1 | class GenericAPIView(views.APIView): |
这下我们直接在 Serializer 里用就行了,GeneticAPIView 一行都不用修改。
对于第二个问题,查询资料发现,DRF 序列化的函数为 to_representation
,其定义如下:
1 | class Serializer(BaseSerializer, metaclass=SerializerMetaclass): |
我们只需要重写它即可,把上面的代码复制下来,然后在 return 之前判断用户身份,替换 ret
的 student_id
和 username
字段。
不对,连复制都不需要,我们只需要调用父类的 to_representation()
,然后加两行就可以了!
1 | def to_representation(self, instance): |
serializers.ListField 和 models.ManyToManyField 的梦幻联动
对于外键这类东西,Serializer 虽然不能自动处理,但是也提供了接口,只要加一点代码就可以完成灵活的写入。
背景
场景是这样的:主讲人
是关于 Activity
和 User
的一个多对多的关系。在 Model 中是这样定义的:
1 | class Activity(models.Model): |
对于获取 Actiivty 信息的 REST API,常见的一种写法是返回该 Activity 的所有主讲人的 id 组成的数组。
在 DRF 中可以使用 ListField 来实现。
1 | class ActivitySerializer(serializers.ModelSerializer): |
但是 ListField 并不能直接将 ManyToManyField 的内容转化为 ListField。转化部分需要我们自己写。
怎么写呢?改 to_representation
吗?我尝试了一下这个,最终放弃了,因为一调用 super().to_representation
就会尝试把 presenter
序列化。
Python 可以定义一个类的方法为属性,如果定义了 setter
,还可以把外界对这个属性的修改,反作用到原来的属性:
1 | class Activity(models.Model): |
然后我们指定一下 ListField 的 source
就可以了:
1 | class ActivitySerializer(serializers.ModelSerializer): |
就这么简单。
对了,后面发现,create
的时候还是会报错,因为 DRF 会在创建 Activity 时尝试设定 presenter
值,而此时 Activity 还没有写入数据库、没有主键,于是也没法创建多对多的记录。
解决方法是重载 create
方法,在 Activity 创建之后再写 presenter:
1 | class ActivitySerializer(serializers.ModelSerializer): |
REST API 标准
开发 REST 的工具有了,那标准呢?这是一个非常重要的问题。就像学了 C 语言后能写出很多的程序,但是常用的代码风格、代码库依旧要参考其他的标准。
我为此新开了一篇博文:RESTful API 标准。
DRF 项目部署
可以参考 https://github.com/uestc-msc/uestcmsc_webapp_backend/blob/lyh543/docs/deploy/deploy.md
Swagger 文档生成
可以看 drf-yasg。