Django REST framework知识点总结

Posted Liatsce

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Django REST framework知识点总结相关的知识,希望对你有一定的参考价值。

本文中会介绍Django REST framework(后续统称DRF)中一些常用功能的使用方法以及框架中的基础概念。希望这些内容能够帮忙大家更好的运用该框架去实现API服务。
本文适合那些已经对Django框架、DRF以及RESTful API设计风格有了解的相关人群。关于Django框架,推荐直接去阅读官方文档,目前官方已经推出中文版的文档;对于DRF,笔者依旧还是推荐优先看官方文档(虽然没有中文版);对于RESTful API设计风格,可以查看笔者编写的<RESTful API设计经验总结>这篇文章。
笔者也会将自己的理解在文中进行阐述,这也算是在和大家交流心得的一个过程。若文中有错误的理解和概念,请大家及时纠正;吸纳大家的建议,对于我来说也是很重要的学习过程之一。


1. 序列化

正常情况下,客户端请求中的数据需要经过转换为model对象,并调用model对象的相关方法实现数据落地。反之,从数据库中获取到的数据(model)需要经过将model对象转换为python的基本数据结构后,交给view层逻辑进行包装并返回给客户端。
上述情况基本上会发生在每一个业务请求逻辑中,而这些操作逻辑基本上都相似。针对这种情况,应该思考是否可以把上述逻辑抽象出来作为通用逻辑;这样即可以减少重复编码,同时还可以规范数据转换的操作细节。在DRF中,就是使用序列化器来实现这种理念的。DRF中的序列化器同时肩负着序列化与反序列化的责任。
还有一点需要注意:本章节所谈的序列化器只负责model与python的基本数据结构之间的转化,而非客户端请求中等的数据于python的基本数据结构之间的转化。

1.1 实现序列化器的思路

本章节中会介绍一些笔者在实现DRF的序列化器时所使用到的技巧和思路。对于实现序列化器的基本流程,笔者推荐去阅读官方文档进行学习。

1.1.1 只针对需要反序列化的字段进行操作

在针对某一个model编写序列化器之前,可以先对其中的字段进行分析。因为有些字段可能不需要返回给客户端,只是用于API服务内部逻辑处理使用或后台逻辑使用。要注意哪些仅仅是内部使用的字段,这些字段是没有必要反序列化的(对外展示)。
同时,也会有一些字段是只有在反序列化的时候是需要的。这是可以给该字段添加上如下属性:

records = serializers.ListField(write_only=True)

1.1.2 控制序列化的字段数量

序列化与反序列化时可能会使用到不同的字段,因此可以使用read_only与write_only属性来进行控制。
例如:

records = serializers.ListField(write_only=True)
id = serializers.IntegerField(read_only=True)

1.1.3 为可选字段设定default值

如果想做到每次只更新部分字段的功能,那么可以为相应的字段加上default值(一般为None)。
这样在后续的逻辑中就可以根据该字段是否为默认值来判断前端是否传入了该字段。

1.1.4 隐藏数据库字段名

如果不想把数据库字段名称对外暴露的话,可以使用source参数为指定model字段起一个用于外部使用的别名。

name = serializers.CharField(source="outside_name", read_only=True)

1.1.5 对关联字段的序列化定义

使用SerializerMethodFiled()定义字段,同时编写一个配套的get_<序列化字段名>的方法。
自定义字段的序列化逻辑,一般用于定义o2m和m2m类型关联字段的序列化逻辑。

class ChargeReadOnlySerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    name = serializers.SerializerMethodField(read_only=True)
    third_party_service = serializers.SerializerMethodField(read_only=True)

def get_name(self, obj) -> int:
    return Charge.ChargeType(obj.c_type).label

def get_third_party_service(self, obj):
    result = 
        id: obj.third_service.id,
        name: obj.third_service.tps_name,
    
    return result

1.1.6 数据校验

反序列化时需要进行数据校验,方法is_vaild()只有反序列化的时候才需要调用。
可以针对每一个字段编写对应的校验方法,也可以对所有字段编写统一的校验方法(该方法中还可以加入自定义逻辑来满足特殊需求)。


1.1.7 对于何时使用serializers.ModelSerializer

可以在如下情况中考虑使用:

  1. 当一个序列化器只用做反序列化时,则可以考虑通过继承serializers.ModelSerializer来快速实现该序列化器。
  2. 当一个model没有多对多关系时,可以考虑使用模型序列化器自动生成。

1.1.8 Model逻辑封装

可以将model相关的逻辑在序列化器中(validate,create或者update)实现。因为序列化器本身就和model关系密切,因此将model相关的封装写到其内部也合理。同时,这样也能大大精简view的编写。

1.1.9 关于Update方法的实现

为了尽可能的减少对数据库的操作,可以采取如下措施:

  1. 在update方法中逐个检查参数是否合法,若不合法则直接返回参数不合法异常。
  2. 在使用序列化器时可加入partial=True,允许部分字段更新(但这可能也需要前端的相关逻辑支持)。

核心理念即为尽量在数据入库之前进行参数合规检查,尽量不要让数据库去检查字段值是否合法;这样可以尽可能的降低数据库压力。

1.1.10 对于反序列化时使用到的字段的定义

建议这一类的字段都加上required=False, default=None这两个参数,以便于前端更灵活的传参和后续逻辑更简单的编写。

1.1.11 自定义数据不合法的返回

重写validate()方法即可。
如果没有将所有的字段都自定义校验,则最好是调用父类的validate()方法:

return super().validate(attrs)

1.1.12 校验必填字段的方式

  1. 利用serializers field的required参数
    使用这种方式时,如果create和update操作涉及到的必填字段不同,那么就要为两种逻辑建立两个write_only=true的字段。即每种操作使用不同的字段去处理逻辑。
  2. 利用validate()方法与serializers field的default参数
。这种方法即是在validate()方法中检查必填参数的值是否为default的值;这种方法比较适用于create和update操作涉及到的必填字段相同。
  3. (推荐)将检验的逻辑放入create()方法与update方法中。这种方法即是将校验必填字段的逻辑移入到create()方法与update方法中,在方法中检查validated_data中是否包含有相应必填参数的key。这么做的前提是必填字段的不能带有default参数以及required=True。

1.1.13 关于数据校验

  1. 一般场景
    数据校验一般是在创建model实例或者是更新model实例时使用。用于检查数据是否合法,符合各个字段的要求。
  2. 检查数据内容是否符合特殊要求
    检查数据的内容是否符合特殊需求,如果符合则触发特殊逻辑。而并非时检查数据是否合法。
  3. 关于校验方法validate
    该方法的形参attrs中的内容,依据的是当前序列化类描述的字段。即如果有某个字段,序列化类没有声明,那么attrs里也不会有这个参数。
  4. 关于序列化类中声明的字段名
    如果继承的是rest_framework.serializers.Serializer:序列化类中声明的字段名必须要与反序列化时传入的各个参数名相互对应;
    如果继承的是rest_framework.serializers.ModelSerializer:序列化类中声明的字段名必须要与相应的model类的各个字段名相互对应(如果没有使用source参数)

1.2 序列化使用流程

使用方法:

  1. 先获取model类实例对象
    若为新建对象:直接调用model的create()方法。
    若为查询或者修改操作:可通过url中的主键或其它查询数据来获取
  2. 在自定义view类中新建序列化类实例
    使用对应的model类对象实例来创建。
    如果是批量序列化,则需要添加many=True参数;同时传入多个对象(query_set)即可。
  3. 序列化后的数据
    序列化类实例的data属性为一个dict,其中便是该model类实例对应的内容。
  4. 返回体数据序列化
    如果使用DRF自带的Response对象,则不需要自行对data进行序列化。(因为DRF封装的Response会根据客户端类型自动按需序列化data内容)。
    若需要自定义返回体数据的序列化方式,则需要自行实现Response对象并将自定义序列化逻辑封装在其内部。

1.3 反序列化使用流程

使用方法:

  1. 先获取model类实例对象
    若为新建对象:直接调用model的create()方法。
    若为查询或者修改操作:可通过url中的主键或其它查询数据来获取
  2. 在自定义view类中新建序列化类实例
    若为新建对象:不需要model类实例,需要修改的数据
    若为修改对象:需要model类实例与修改的数据。
  3. 调用校验方法is_valid
  4. 调用序列化类实例的save方法

2. ViewSet

2.1 ViewSet的优势

如果不使用ViewSet,而是使用APIView(或使用Django CBV)来实现view层,那么只能实现Http method的同名方法(get,put等等)。
如果使用ViewSet来编写view,则可以将业务逻辑方法通过action绑定到对应的Http method同名方法。利用该功能,就可以把同类型的业务逻辑都封装在同一个ViewSet类中,即ViewSet与业务逻辑可建立对应关系,刚便于代码的整理与阅读

2.2 自定义ViewSet

如果不想使用GenericAPIView使用的mixin类,但又想自定义Http method方法名时,可以`让自定义view继承ViewSetMixin和APIView即可。

2.3 关于ModelViewSet

由于该视图类继承的mixin中的各个操作方法,都是写好的固定逻辑;因此肯定会有不符合自定义逻辑的地方。
此时,可以尝试仿写rest_framework.mixins中的各个方法;其次再让自定义view继承自定义的mixin类与GenericViewSet类;这样就可以按照类似于ModelViewSet的方式去写view了。

2.4 关于Mixin

在Django以及DRF内部,有很多的CBV都使用了Mixin来实现的。包括笔者在使用这两者去实现项目需求时,也会仿照它们去实现一些Mixin类。通过实践,笔者也体会到了Mixin的一些特点与优势,在这里给大家分享一下。

Mixin是利用了Python的多重继承机制来实现的

Mixin的目的就是给一个类增加多个功能。这样,在设计类的时候,我们优先考虑通过多重继承来组合多个Mixin的功能,而不是设计多层次的复杂的继承关系。可以将常用的逻辑进行通用化的抽象,并将这些逻辑封装到Mixin中。当view需要使用到这些功能时,就可以直接继承相应的Mixin类并直接调用相应的方法即可。

  • 使用Mixin模式有几点是需要注意:

笔者对上述几点的理解是:Mixin里实现的不是一个子类,而是指封装了一些功能方法的工具类。继承Mixin实际上是为了给子类添加更多的功能,而不是传统的继承父类。即Mixin类是无需实例化的,Mixin类无法单独作为一个类进行对象实例化使用。

对于Mixin的使用细节,这里笔者谈及一点:在继承mixin类时,务必要把所有的mixin类写在继承队列的最前端。这主要是由于Python的多重继承是通过C3算法实现的。如果所有类中没有同名的方法,那可以不用按照上述的要求进行编写。但


3. 自定义认证逻辑

3.1 实现思路

  1. 自定义认证类需要继承rest_framework.authentication.BaseAuthentication

  2. 需要重写authenticate方法以及authenticate_header方法(可选)。
    authenticate方法中主要是写自定义认证的逻辑。例如从request请求中获取token,从而判断token的合法性等等。

3.2 配置

全局开启:在setting中配置相关参数。

REST_FRAMEWORK = 
    "DEFAULT_AUTHENTICATION_CLASSES": [zonebank.auth.ZoneBankAuthentication]

局部开启:在继承了APIView的视图中使用authentication_classes类属性来配置(实际上为APIView的类属性)。类属性类型为list,内容为自定义的认证类名。
局部关闭:在继承了APIView的视图中使用authentication_classes=[]来关闭全局开启认证

3.3 使用方式

  1. 通过drf的request来使用
    因为自定义认证类中的authenticate方法返回的tuple中的内容会相应的保存到DRF Http request的user和auth中属性中。
    因此可以直接在后续的代码中通过使用request.user或request.auth来使用自定义认证返回的数据。

4. 自定义权限

配置的方式类似于认证配置;APIView相关类属性:permission_classes


5. 自定义解析器

解析器是用来将前端传送过来的数据转化为python的基本数据结构。

DRF自带了Json,File和html from表单的parser。如果传给api的数据格式比较特殊,则可以建立自定义的解析器来解析


6.自定义异常

通过重写DRF中views.exception_handler方法来实现,该方法最后应返回一个Response
若想直接复用DRF已有的异常处理机制,则可以在自定义的exception_handler方法中调用DRF的exception_handler,之后在新增的自定义的异常处理逻辑。
若不想使用DRF的默认异常响应,则需要自行对DRF的自带异常进行捕获、分析以及构造自定义Response;同时,以同样的方式对自定义的异常进行操作。
针对于上述情况,笔者这里给出一个实践思路,供大家参考:

  1. 首先为特定的异常编写异常处理器

    class AuthErrorHandler(BaseAPIExceptionHandler):
    
    def handle(self):
        response = Response(
            
                ErrorCode: LOGIN-AUTH-401,
                ErrorMessage: str(self.exc)
            ,
            status=status.HTTP_401_UNAUTHORIZED
        )
    
        return response
  2. 将第一步中实现的异常处理器注册到异常处理注册表中

    # 需特殊处理的异常
    # 以dict的形式注册到该list中
    # dict中,key:exception的value为异常类,key:exception的value为相应的异常处理器
    error_type = [
    
        exception: BaseAPIException,
        handler: ZoneBankErrorHandler
    ,
    
        exception: AuthenticationFailed,
        handler: AuthErrorHandler
    ,
    
        exception: ValidationError,
        handler: SerializerErrorHandler
    ,
    
        exception: ParseError,
        handler: RequestParseErrorHandler
    ,
    
        exception: MethodNotAllowed,
        handler: MethodNotAllowedErrorHandler
    ,
    ]
  3. 编写自定义exception_handler方法

    def api_exception_handler(exc, context):
    response = None
    
    # 处理特殊异常
    for error in error_type:
        if isinstance(exc, error[exception]):
            custom_handler = error[handler](exc, context)
            response = custom_handler.handle()
    
    # 处理未知错误
    if not response:
        # response = exception_handler(exc, context) # 执行drf自带异常处理
        # if response is not None:
        default_handler = DefaultErrorHandler(exc, context)
        response = default_handler.handle()
    
    return response

7. 自定义Http Response

通过继承rest_framework.response.Response,并重写其构造方法即可。注意还需要调用父类的构造方法。

例如:

from rest_framework.response import Response

class APIResponse(Response):
    """Http response for API
    """

    def __init__(
        self, 
        message: str = None,
        data:dict = None,
        http_status: int = 200,
        headers:dict = None
    ):
        """init
        """
        result = 

        # set response message
        if message:
            result[Message] = message
        else:
            result[Message] = Request Successfully

        # set response data
        if data:
            result[Data] = data

        # 调用父类构造方法
        super().__init__(data=result, status=http_status, headers=headers)

8. 实现JWT用户身份认证功能

需要提前安装djangorestframework_jwt第三方依赖包。

8.1 自定义JWT认证类

JWT认证类主要是用来解析客户端中带有的JWT,即做的是检查请求的用户身份合法性

这里提供两种实现思路:

  1. 如果想自定义认证异常返回消息以及获取JWT的方式时,则直接继承BaseJSONWebTokenAuthentication然后重写authenticate方法即可。注意authenticate方法需要返回一个长度为2的Tuple类型变量,并且第一个元素必须是django user类的对象实例。
  2. 如果只是想修改jwt的获取方式时,则自定义JWT认证模块的方式为继承BaseJSONWebTokenAuthentication然后重写get_jwt_value方法即可。该思路其实就是在模仿JWT模块自带的那个JSONWebTokenAuthentication方法来实现。

在实现相关方法时,可以使用rest_framework_jwt.settings.api_settings.JWT_DECODE_HANDLER方法来解析出Token中的payload部分。并且,若使用的是Django的user model,则还可以调用rest_framework_jwt.settings.api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER方法来获取payload中的username值,以用于对获取user model对象。

若想自定义JWT接口的Response,则可以重写rest_framework_jwt.utils.jwt_response_payload_handler方法。重写方式见源码方法注释内容(前提:如果使用的是自动签发功能)。

认证类在实现时可以仿照rest_framework_jwt.authentication.BaseAuthentication来编写,修改其中的日志记录以及错误抛出即可。

  • 大致实现逻辑如下:

这里给出笔者曾经实践的认证类,供大家参考:

import jwt

from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.authentication import BaseAuthentication

logger = settings.LOGGER

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER

def get_authorization_header(request):
    """获取request中的jwt;支持从cookie中获取
    """
    auth = request.META.get(HTTP_AUTHORIZATION, b)
    if not auth:
        auth = request._request.COOKIES.get(jwt, b)
    if isinstance(auth, str):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth

class JWTAuthentication(BaseAuthentication):
    """JWT认证类
    """

    def authenticate(self, request):
        """JWT认证逻辑
        """
        jwt_value = get_authorization_header(request)

        if not jwt_value:
            logger.info(log_msg_to_json_str(
                msg=获取JWT失败:request header的Authorization字段未包含token;,
                data=request.data,
                client_ip=request.META.get(REMOTE_ADDR, )
            ))
            raise AuthenticationFailed(登陆验证失败.请提供合法的登陆验证信息)

        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignatureError:
            logger.info(log_msg_to_json_str(
                msg=JWT验证失败:JWT已过期;JWT:0.format(jwt_value),
                data=request.data,
                client_ip=request.META.get(REMOTE_ADDR, )
            ))
            raise AuthenticationFailed(登陆信息已过期.请尝试重新登陆)
        except jwt.DecodeError:
            logger.warning(log_msg_to_json_str(
                msg=JWT验证失败:解析payload失败;JWT:0.format(jwt_value),
                data=request.data,
                client_ip=request.META.get(REMOTE_ADDR, )
            ))
            raise AuthenticationFailed(登陆验证失败.请提供合法的登陆验证信息)
        except jwt.InvalidTokenError:
            logger.warning(log_msg_to_json_str(
                msg=JWT验证失败:接收到非法token;JWT:0.format(jwt_value),
                data=request.data,
                client_ip=request.META.get(REMOTE_ADDR, )
            ))
            raise AuthenticationFailed(非法登陆.请提供合法的登陆验证信息)

        user = self.authenticate_credentials(payload, request)

        return (user, jwt_value)

    def authenticate_credentials(self, payload, request):
        """验证jwt payload中用户信息的合法性
        """
        user_model = get_user_model()
        username = jwt_get_username_from_payload(payload)

        if not username:
            logger.warning(log_msg_to_json_str(
                msg=JWT验证失败:无法获取payload中的username,
                data=request.data,
                client_ip=request.META.get(REMOTE_ADDR, )
            ))
            raise AuthenticationFailed(非法登陆.请提供合法的登陆验证信息)

        try:
            user = user_model.objects.filter(name=username).first()
        except user_model.DoesNotExist:
            logger.info(log_msg_to_json_str(
                msg=JWT验证失败:用户名:0不存在.format(username),
                data=request.data,
                client_ip=request.META.get(REMOTE_ADDR, )
            ))
            raise AuthenticationFailed(登陆信息认证失败.用户名不存在)

        if not user.is_active:
            logger.info(log_msg_to_json_str(
                msg=JWT验证失败:用户:0未激活.format(username),
                data=request.data,
                client_ip=request.META.get(REMOTE_ADDR, )
            ))
            raise AuthenticationFailed(登陆信息认证失败.该用户名未激活,请联系客户激活该用户后再尝试登陆)

        return user

8.2 签发JWT

用户在首次请求时,需要先使用个人的用户名以及密码来请求获取JWT。平台在验证用户的身份后会自动生成相应的JWT返回给用户,这样用户在后续的请求中就可以使用JWT来作为身份信息了。
基于上述逻辑,一般需要建立一个专用于下发JWT的API。这里提供一些笔者的实践经验供大家参考。

8.2.1 实现JWT签发(生成)逻辑

由于客户端是通过请求相应的Auth API来获取JWT,因此JWT的签发(生成)逻辑可以在相应的序列化器中来实现。同时,可以通过将签发逻辑封装在序列化器的validate方法中;这样在签发JWT时就与序列化器的参数校验方法结合了起来,方便上层view的调用。

使用rest_framework_jwt.settings.api_settings.JWT_PAYLOAD_HANDLER方法来生成jwt的payload部分。其中,需要将对应的user对象传给jwt_payload_handler方法。其次,使用rest_framework_jwt.settings.api_settings.JWT_ENCODE_HANDLER方法生成完整的JWT。

另外,该序列化器是不需要实现create和update方法的。因为不需要该序列化器去保存或修改model对象。

  • 大致实现思路如下:

8.2.2 实现JWT签发接口

完成含有签发JWT逻辑的序列化器后,还需要使用它来实现签发JWT API所对应的View。

首先,在实现该API View时,务必需要将其类属性authentication_classes至为空List。因为如果没有做该操作,则该View在执行相关逻辑前会先去调用章节8.1中所定义的JWT认证类;但该API又为JWT签发接口,因此就会与其需求产生矛盾并导致无法正常签发JWT签发。

其次,由于之前已经将JWT签发逻辑封装在对应的序列化器的validate方法中,那么在View中实现签发逻辑时就可以直接调用序列化器的参数校验逻辑。即view中不会出现其他不相关的逻辑,与普通的view相同,只是调用了序列化器和相关日志记录等。

最后,在成功生成了JWT后,可通过使用自定义Http response或DRF的APIResponse来将JWT下发给用户。

这里给出笔者曾经实践的view,供大家参考:

class LoginAPIView(ViewSet):
    """
    登陆API视图
    """
    authentication_classes = []

    def login(self, request):
        jwt_serializer = JWTLoginSerializer(data=request.data)
        try:
            jwt_serializer.is_valid(raise_exception=True) # 校验用户以及签发JWT
        except ValidationError as error:
            logger.warning(log_msg_to_json_str(
                msg=用户登陆失败;错误原因:数据校验不通过;exc_type:0;details:1.format(
                    type(error), error
                ),
                data=request.data
            ))      
            raise LoginError(detail=用户登陆失败;提供的用户数据不合法;Details:0.format(error))
        jwt = jwt_serializer.context.get(jwt)
        user = jwt_serializer.context.get(user)

        return APIResponse(msg=登陆成功, http_code=200, username=user.name, token=jwt)

8.3 注册自定义认证类

最后还需要将自定义认证类注册到DRF中:

REST_FRAMEWORK = 
    "DEFAULT_AUTHENTICATION_CLASSES": ["tsmp.auth.JWTAuthentication"]

这种注册为全局注册,即所有的view都会使用其作为用户认证逻辑。


9. DRF Http Request

DRF为其自身的功能特性而重新封装了Django的Http Request对象。DRF的Http Request对象是基于Django的Http Request对象而实现的,因此对于Django Http Request对象的一些操作也可以作用于DRF Http Request对象上。下面罗列一些DRF Http Request的使用技巧:

  1. 获取Django Http Request对象
    原生的django request可使用DRF request来获取:
    django_request = request._request
  2. DRF的Request与django原生的request使用方式相同
    其原因是DRF的request写了getattr方法。该方法中对django原生的request进行了反射调用(getattr())。
  3. DRF Http Request body data

    GET:

    文件:

以上是关于Django REST framework知识点总结的主要内容,如果未能解决你的问题,请参考以下文章

Python前后端分离开发Vue+Django REST framework实战_Django REST framework框架

视图集中视图的 Django Rest Framework 自定义模式

Django REST Framework 缓存错误

Python前后端分离开发Vue+Django REST framework实战

Python前后端分离开发Vue+Django REST framework实战

20-Django REST framework-Serializer序列化器