DRF全局异常处理及其他插件

DRF项目全局异常处理

exception_handler

# 全局异常处理

# 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',

在后端开发中,无论是视图类的方法出现异常,还是其他地方出现异常,都可以通过全局异常处理来进行统一的处理

例如:给定的python代码片段展示了一个全局异常处理函数,它会处理所有视图类方法出现的异常

  • 通过配置 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
    • 当发生异常时,系统会调用该函数进行异常处理
  • 在该函数中,可以执行一些自定义的操作来处理异常
    • 比如记录日志,返回统一的错误响应等

源码:

def exception_handler(exc, context):
    """
    Returns the response that should be used for any given exception.

    By default we handle the REST framework `APIException`, and also
    Django's built-in `Http404` and `PermissionDenied` exceptions.

    Any unhandled exceptions may return `None`, which will cause a 500 error
    to be raised.
    """
    if isinstance(exc, Http404):
        exc = exceptions.NotFound()
    elif isinstance(exc, PermissionDenied):
        exc = exceptions.PermissionDenied()

    if isinstance(exc, exceptions.APIException):
        headers = {}
        if getattr(exc, 'auth_header', None):
            headers['WWW-Authenticate'] = exc.auth_header
        if getattr(exc, 'wait', None):
            headers['Retry-After'] = '%d' % exc.wait

        if isinstance(exc.detail, (list, dict)):
            data = exc.detail
        else:
            data = {'detail': exc.detail}

        set_rollback()
        return Response(data, status=exc.status_code, headers=headers)

    return None

补充:

isinstance() / issubclass()

是两个内置的用于判断对象与类之间的关系函数

isinstance() 函数用于检查一个对象是否是一个类的实例

  • 它接受两个参数

    • 第一个参数是待检查对象
    • 第二个是类或类型
  • 如果对象是第二个参数指定的类或类型的实例

    • 则返回True
    • 否则返回False
    class Person:
        pass
    
    person = Person()
    print(isinstance(person, Person))  # 输出:True
    print(isinstance(person, object))  # 输出:True
    print(isinstance(person, str))     # 输出:False
    

issubclass() 函数用于检查一个类是否是另一个类的子类

  • 它接受两个参数
    • 第一个参数是待检查的类
    • 第二个参数是父类或超类

自定义异常

创建异常处理函数

# 自定义全局异常处理函数
from rest_framework.views import exception_handler
from rest_framework.response import Response

from algo_platform.middleware import logger


def common_exception_handler(exc, context):
    '''
    exception_handler:只处理了 DRF 的异常,其他的异常不会被捕获
    :param exc:
    :param context:
    :return:
    '''
    # 一般出现异常,都会进行日志的记录

    # 定义返回异常格式 存储相关异常信息
    back_dict = {"code": -1, "message": ""}

    # 如果异常对象是 DRF 的 APIException对象,则会返回 Response
    # 如果异常对象不是 DRF 的 APIException对象,则会返回 None

    # 执行原来的 exception_handler 函数会捕获到异常 并且返回 Response 或 None
    # 会捕获DRF的APIException对象并返回Response
    # 如果不是DRF的APIException对象,则返回None
    result = exception_handler(exc, context)

    if result:
        # 有值 Response 则说明是DRF的异常,已经被捕获和处理了
        back_dict['code'] = 996
        # 异常返回有两种格式,一种是字典,一种是直接的数据
        if isinstance(result.data, dict):
            # 异常是字典格式
            back_dict["detail"] = result.data.get('detail')
        else:
            # 异常是直接的数据
            print(result.data)
            back_dict["detail"] = result.data

        back_dict["message"] = "程序异常,请联系管理员处理异常!"
        # back_dict["message"] = str(exc)  # 这种错误展示会将具体的错误信息展示出来,一般给客户做东西,客户可以提交这个信息便于排查问题
        return Response(back_dict)
    else:
        # 无值 None 则说明是 其他的异常,没有被处理
        back_dict["message"] = "系统异常,请联系管理员处理异常!"
        logger.error(str(exc))
        return Response(back_dict)

修改配置文件

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'app01.excepiton.my_excepiton.common_exception_handler',
}

DRF过滤

准备数据

from django.db import models

# Create your models here.
class Book(models.Model):
    name = models.CharField(max_length=32)
    price = models.IntegerField()
# -*-coding: Utf-8 -*-
from app01 import models
from rest_framework import serializers


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Book
        fields = "__all__"

过滤

  • 在RESTful规范中

    • 可以将过滤条件作为请求地址的一部分来实现
    • 通常,在RESTful API中,有一个用于查询所有资源的接口,可以使用过滤条件对这些资源进行筛选

    例如

    考虑一个资源为用户的API,我们可以使用以下方式来实现带过滤条件的查询所有用户的接口

    GET /users?filter=条件

    /users是资源的基本路径,filter=条件表示过滤条件。

    带过滤的接口只有:查询所有

使用

搜索某个字段中带有某个参数

内置过滤类

# (2) 过滤:必须继承 GenericAPIView 及其子类,才能使用(如果继承APIView,则不能这么写)
# DRF 给我们提供了一个排序类  使用该类进行搜索过滤 SearchFilter 
from rest_framework.filters import SearchFilter

class BookView(GenericViewSet, ListModelMixin):
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer
	# 指定过滤器后端,将SearchFilter添加到过滤器列表中
    filter_backends = [SearchFilter]
    # SearchFilter:必须配合一个类属性
    # 按照那些字段进行筛选
    # http://127.0.0.1:8000/app01/v1/books/?search=梦
    # 只要name/price中带 梦 都能被搜出来
    
    # 指定进行搜索过滤的字段,这里以name字段作为搜索条件
    search_fields = ['name']
   
http://127.0.0.1:8000/app01/v1/books/?search=梦
[
    {
        "id": 5,
        "name": "小梦梦睡衣",
        "price": 66666
    },
    {
        "id": 6,
        "name": "追梦赤子心",
        "price": 555
    }
]
  • 查询集在models.Book中,即对应的是Book表或模型。
  • search_fields指定了搜索字段为name
  • DRF会自动解析请求中的搜索参数search的值,即
  • 在查询集中,会对name字段进行匹配,找出包含的资源对象。

第三方过滤类

# (2) 过滤:必须继承 GenericAPIView 及其子类,才能使用(如果继承APIView,则不能这么写)
# 第三方过滤类: pip3.9 install django-filter
from django_filters.rest_framework import DjangoFilterBackend


class BookView(GenericViewSet, ListModelMixin):
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [DjangoFilterBackend]  # 排序类
    # SearchFilter:必须配合一个类属性
    # 按照那些字段进行筛选
    # http://127.0.0.1:8000/app01/v1/books/?search=梦
    # 根据指定字段进行筛选,按名字和价格精准匹配
    filterset_fields = ['name', 'price']

{{host}}app01/v1/books/?name=追梦赤子心

[
    {
        "id": 6,
        "name": "追梦赤子心",
        "price": 555
    }
]
  • 查询集在models.Book中,即对应的是Book表或模型。
  • filterset_fields指定了过滤字段为nameprice
  • DjangoFilterBackend会自动解析请求中的过滤参数name的值,即追梦赤子心
  • 在查询集中,会根据name字段进行精确匹配过滤,找出名字为追梦赤子心的资源对象。

自定义过滤类

# -*-coding: Utf-8 -*-
# @File : book_filter .py
from rest_framework import filters
from django.db.models import Q


class BookFilter(filters.BaseFilterBackend):
    '''
        def filter_queryset(self, request, queryset, view):
        """
        Return a filtered queryset.
        """
        raise NotImplementedError(".filter_queryset() must be overridden.")
    '''

    def filter_queryset(self, request, queryset, view):
        # 返回的数据,都是过滤后的数据
        # http://127.0.0.1:8000/app01/v1/books/?price=99&name=追梦赤子心
        # 需要将筛选条件变成price=99或name=追梦赤子心,而第三方写法中访问地址只能是上面的格式
        price = request.query_params.get('price')
        name = request.query_params.get('name')
        # 使用了Q对象来构建查询条件
        # 将name和price参数作为过滤条件进行查询操作
        # 并返回符合条件的结果集。
        queryset = queryset.filter(Q(name=name) | Q(price=price))

        return queryset
# (2) 过滤:必须继承 GenericAPIView 及其子类,才能使用(如果继承APIView,则不能这么写)
from django_filters.rest_framework import DjangoFilterBackend


class BookView(GenericViewSet, ListModelMixin):
    queryset = models.Book.objects.all()
    # 自定义过滤类
    serializer_class = BookSerializer
    filter_backends = [BookFilter]  # 排序类

{{host}}app01/v1/books/?name=追梦赤子心

[
    {
        "id": 6,
        "name": "追梦赤子心",
        "price": 555
    }
]
  • 当请求参数为{{host}}app01/v1/books/?name=追梦赤子心
  • 将返回name字段为"追梦赤子心"的数据。

{{host}}app01/v1/books/?price=999

[
    {
        "id": 4,
        "name": "d",
        "price": 999
    }
]
  • 当请求参数为{{host}}app01/v1/books/?price=999
  • 将返回price字段为999的数据。

{{host}}app01/v1/books/?price=999&name=追梦赤子心

[
    {
        "id": 4,
        "name": "d",
        "price": 999
    },
    {
        "id": 6,
        "name": "追梦赤子心",
        "price": 555
    }
]
  • 当请求参数为{{host}}app01/v1/books/?price=999&name=追梦赤子心
  • 将返回同时满足name字段为"追梦赤子心"或price字段为999的数据。

小结

  • 过滤类和排序类可以多个一起使用
  • 当有多个过滤条件时,建议将过滤条件数量较多的放在最前面
  • 这样可以减少后续的数据处理量,提高查询的性能

DRF排序

  • restful规范中
    • 请求地址中带过滤条件
  • 排序功能的接口:查询所有
  • 必须继承GenericAPIView及其子类
    • 才能使用排序类
    • DRF提供了有一个排序类
  • 如果继承APIView则不能这么使用
  • 路由
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import SimpleRouter
from app01 import views

router = DefaultRouter()

router.register("books", views.BookView, 'books')
urlpatterns = [
    path('admin/', admin.site.urls),
    path("app01/v1/", include(router.urls))
]
  • 视图
# Create your views here.
# (1)排序:必须继承 GenericAPIView 及其子类,才能使用(如果继承APIView,则不能这么写)
# DRF 给我们提供了一个排序类
from rest_framework.filters import OrderingFilter


class BookView(GenericViewSet, ListModelMixin):
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [OrderingFilter]  # 排序类
    # OrderingFilter:必须配合一个类属性
    # 按照那些字段进行排序 
    # 先按价格倒序排,价格一样,再按id倒序排
    # http://127.0.0.1:8000/api/v1/books/?ordering=-price,-id
    ordering_fields = ['price', 'id']

{{host}}app01/v1/books/

[
    {
        "id": 1,
        "name": "a",
        "price": 44
    },
    {
        "id": 2,
        "name": "b",
        "price": 666
    },
    {
        "id": 3,
        "name": "c",
        "price": 88
    },
    {
        "id": 4,
        "name": "d",
        "price": 999
    }
]

{{host}}app01/v1/books/?ordering=price

[
    {
        "id": 1,
        "name": "a",
        "price": 44
    },
    {
        "id": 3,
        "name": "c",
        "price": 88
    },
    {
        "id": 2,
        "name": "b",
        "price": 666
    },
    {
        "id": 4,
        "name": "d",
        "price": 999
    }
]

{{host}}app01/v1/books/?ordering=price,-id

[
    {
        "id": 1,
        "name": "a",
        "price": 44
    },
    {
        "id": 3,
        "name": "c",
        "price": 88
    },
    {
        "id": 2,
        "name": "b",
        "price": 666
    },
    {
        "id": 4,
        "name": "d",
        "price": 999
    }
]

{{host}}app01/v1/books/?ordering=-price,id

[
    {
        "id": 4,
        "name": "d",
        "price": 999
    },
    {
        "id": 2,
        "name": "b",
        "price": 666
    },
    {
        "id": 3,
        "name": "c",
        "price": 88
    },
    {
        "id": 1,
        "name": "a",
        "price": 44
    }
]

DRF分页

  • 查询所有接口,过滤和排序了,但是实际上,这个接口,都需要有分页功能
  • 修改接口:
    • 首先,需要对接口进行修改以支持分页功能。
    • 在接口中添加两个参数:pagepage_size
      • page表示当前页码,
      • page_size表示每页显示的数据数量。
    • 这样你就可以通过这两个参数来分页查询数据。
  • 分页展现形式:
    • 根据不同的展现形式,在Web和App/小程序上采取不同的分页交互方式。
      • Web展现形式:
        • 在Web页面上,通常会使用下一页点击的方式实现分页。
        • 当用户点击下一页按钮时,前端会发送包含page参数的请求,后端根据接收到的page参数返回相应页码对应的数据。
      • App/小程序展现形式:
        • 在App或小程序中,常见的分页交互方式是下滑加载下一页。
        • 当用户滑动到页面底部时,前端会发送请求获取下一页数据。
        • 请求中同样包含page参数,后端根据接收到的page参数返回相应页码对应的数据。
  • 无论是Web还是App/小程序,接口都应该按照相同的规则进行分页处理,即根据pagepage_size参数进行数据的查询和返回。
  • drf提供给咱们,三种分页方式
    • 基本分页
      • 基本分页是最常见和最简单的分页方式之一。
      • 它使用两个参数来确定每页显示的数据数量和当前所处的页码。
      • 通常使用的参数是pagepage_size
        • page表示当前页码
        • page_size表示每页显示的数据数量。
      • 通过在接口请求中传递这两个参数,可以获取相应页码对应的数据。
      • 基本分页适用于小数据量的情况。
    • 偏移分页
      • 偏移分页使用两个参数来确定每页显示的数据数量和从哪个数据位置开始显示。
      • 与基本分页不同的是,偏移分页的参数为pagepage_size
        • page表示当前页码
        • page_size表示每页显示的数据数量。
      • 通过计算起始位置,可以从数据库中查询数据并返回给用户。
      • 偏移分页适用于固定顺序的数据集,但在数据量较大时可能效率较低。
    • 游标分页
      • 游标分页是一种以数据的特定标识作为游标,根据游标的位置来确定每页显示的数据。
      • 每次接收到请求时,服务端会返回当前页的数据以及下一页的游标,客户端可使用该游标来获取下一页的数据。
      • 游标通常是一个与数据相关的唯一标识,如数据库的主键或创建时间等。
      • 游标分页适用于大型数据库和需要实现快速、高效分页的场景。

基本分页

自定义分页

# (1)PageNumberPagination: 基本分页
from rest_framework.pagination import PageNumberPagination

class BookNumberPagination(PageNumberPagination):
    # 重写 4 个 类属性
    page_size = 2  # 每页显示的条数

    page_query_param = 'page'  # 路径后面的 参数:page=4(第4页)
	# 例如 http://127.0.0.1:8000/app01/v1/books/?page=4表示查询第4页的数据。
    
    page_size_query_param = 'page_size'  # page=4&page_size=5:查询第4页,每页显示5条
	# http://127.0.0.1:8000/app01/v1/books/?page=4&page_size=5表示查询第4页的数据,每页显示5条。
    
    max_page_size = 5  # 每页最多显示5条

返回结果说明

  • count:符合查询条件的总记录数,即所有记录的数量。
  • next:下一页的URL链接,如果有下一页数据,则返回对应的URL;否则返回null。
  • previous:上一页的URL链接,如果有上一页数据,则返回对应的URL;否则返回null。
  • results:当前页的数据列表。

偏移分页

自定义分页类

# (2)LimitOffsetPagination:偏移分页
class BookLimitOffsetPagination(LimitOffsetPagination):
    # 重写 4 个 类属性
    default_limit = 2  # 每页显示的条数
    limit_query_param = 'limit'  # limit:3 本页取三条
    offset_query_param = 'offset'  # 偏移量是多少 offset=3&limit:3 : 从第3条开始取3条数据
    max_limit = 5  # 限制每次取的最大条数
# (3) 分页:必须继承 GenericAPIView 及其子类,才能使用(如果继承APIView,则不能这么写)
# # 第三方过滤类: pip3.9 install django-filter
from app01.paginations.book_pagination import BookLimitOffsetPagination

class BookView(GenericViewSet, ListModelMixin):
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer
    # 配置分页器
    # 只能按一种方式分页,不能放到列表中
    pagination_class = BookLimitOffsetPagination

游标分页

自定义分页

# (3)CursorPagination:游标分页
# 只能上一页或下一页,但是速度特别快,经常用于APP上
class BookCursorPagination(CursorPagination):
    # 重写3个类属性
    cursor_query_param = 'cursor'  # 查询参数
    page_size = 2  # 每页显示2条
    ordering = 'id'  # 必须是要分页的数据表中的字段,一般是id
# (3) 分页:必须继承 GenericAPIView 及其子类,才能使用(如果继承APIView,则不能这么写)
from app01.paginations.book_pagination import BookCursorPagination

class BookView(GenericViewSet, ListModelMixin):
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer
    # 配置分页器
    # 只能按一种方式分页,不能放到列表中
    pagination_class = BookCursorPagination

小结

自定义分页

#!/usr/bin/env python
# encoding: utf-8

from rest_framework.pagination import PageNumberPagination
from rest_framework.settings import api_settings
from rest_framework.response import Response
from collections import OrderedDict


class CustomizationPagination(PageNumberPagination):
    page_query_param = 'currentPage'
    page_size = api_settings.PAGE_SIZE
    page_size_query_param = 'pageSize'

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('total', self.page.paginator.count),
            ('pageSize', self.get_page_size(self.request)),
            ('currentPage', int(self.request.query_params.get(self.page_query_param, 1))),
            ('results', data)
        ]))

    def paginate_queryset(self, queryset, request, view=None):
        if request.query_params.get(self.page_size_query_param) in ["0", "no"]:
            return None

        return super(CustomizationPagination, self).paginate_queryset(queryset, request, view=view)

settings中配置:

'DEFAULT_PAGINATION_CLASS': 'algo_platform.pagination.CustomizationPagination',

posted @ 2025-03-10 21:52  小郑[努力版]  阅读(37)  评论(0)    收藏  举报