# 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 官方教程

汉化版教程链接 (opens new window)

# 例:在序列器中定义可修改的外键 id

class Activity(models.Model):
    # 为了防止循环引用,这里使用字符串表示 ActivityPhoto 类
    banner = models.ForeignKey('activities_photos.ActivityPhoto', null=True, default=None, on_delete=models.SET_NULL,
                               verbose_name="首页图", related_name="banner_of")

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 就行了。

class ActivitySerializer(serializers.ModelSerializer):
    class Meta:
        model = Activity
        fields = ("id", "banner_id")
        read_only_fields = ("id")
    # 默认的 banner_id 是 allow_null=False, read_only=True,所以需要显式定义
    banner_id = serializers.CharField(allow_null=True, read_only=False)

    # 默认的也不带 validator,如果不写 validator 导致数据库插入失败,就等着服务器抛 500 吧
    def validate_banner_id(self, banner_id: str):
        if banner_id is None or ActivityPhoto.objects.filter(id=banner_id):
            return banner_id
        raise serializers.ValidationError("id 对应的图片不存在")

Django 永远滴神!

# 例:自定义 APIException

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

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

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

class OnedriveUnavailableException(APIException):
    status_code = 503
    default_detail = 'Onedrive 服务未登录。'


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

太香了!

# 例:自定义对象级权限

文档 (opens new window)

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

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)
        )
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 序列化后返回:

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 之前修改一下,添加一个字段就可以了。

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 部分是一个函数都没有写。

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 中做了查询、分页、序列化、包装成分页形式再返回的操作。

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 删掉。

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 文档 (opens new window) 中提到,可以在序列化时提供 context

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

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,其定义如下:

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(),然后加两行就可以了!

    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

# serializers.ListField 和 models.ManyToManyField 的梦幻联动

对于外键这类东西,Serializer 虽然不能自动处理,但是也提供了接口,只要加一点代码就可以完成灵活的写入。

# 背景

场景是这样的:主讲人 是关于 ActivityUser 的一个多对多的关系。在 Model 中是这样定义的:

class Activity(models.Model):

    presenter = models.ManyToManyField(User, verbose_name="主讲人", related_name="present_activity")

对于获取 Actiivty 信息的 REST API,常见的一种写法是返回该 Activity 的所有主讲人的 id 组成的数组。

在 DRF 中可以使用 ListField (opens new window) 来实现。

class ActivitySerializer(serializers.ModelSerializer):
    class Meta:
        model = Activity

    presenter = serializers.ListField(child=serializers.IntegerField(), read_only=False)

但是 ListField 并不能直接将 ManyToManyField 的内容转化为 ListField。转化部分需要我们自己写。

怎么写呢?改 to_representation 吗?我尝试了一下这个,最终放弃了,因为一调用 super().to_representation 就会尝试把 presenter 序列化。

Python 可以定义一个类的方法为属性,如果定义了 setter,还可以把外界对这个属性的修改,反作用到原来的属性:

class Activity(models.Model):

    presenter = models.ManyToManyField(User, verbose_name="主讲人", related_name="present_activity")

    @property
    def presenter_id(self) -> List[int]:
        queryset = self.presenter.all().values('id')
        return list(map(lambda u: u['id'], queryset))

    @presenter_id.setter
    def presenter_id(self, value: List[int]):
        self.presenter.clear()
        self.presenter.add(*value)

然后我们指定一下 ListField 的 source 就可以了:

class ActivitySerializer(serializers.ModelSerializer):
    class Meta:
        model = Activity

    presenter = serializers.ListField(child=serializers.IntegerField(), source="presenter_id", read_only=False)

就这么简单。


对了,后面发现,create 的时候还是会报错,因为 DRF 会在创建 Activity 时尝试设定 presenter 值,而此时 Activity 还没有写入数据库、没有主键,于是也没法创建多对多的记录。

解决方法是重载 create 方法,在 Activity 创建之后再写 presenter:

class ActivitySerializer(serializers.ModelSerializer):
    class Meta:
        model = Activity

    presenter = serializers.ListField(child=serializers.IntegerField(), source="presenter_id", read_only=False)

    def create(self, validated_data):
        presenter = validated_data.pop('presenter', [])
        activity = super().create(validated_data)
        activity.presenter_id = presenter
        return activity

# REST API 标准

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

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

# DRF 项目部署

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

# Swagger 文档生成

可以看 drf-yasg