1. 程式人生 > 實用技巧 >drf的過濾排序分頁異常處理

drf的過濾排序分頁異常處理

一. 過濾元件#

1. 步驟#

Copy
1. 安裝:pip3 install django-filter
2. 註冊: settings.py中註冊
    INSTALLED_APPS = [
	    ...
    	'django_filters',  # 需要註冊應用,
	]
    
3. 全域性配置 或者 區域性配置
    全域性配置: 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
    區域性配置:
        指定所有欄位: filter_fields = '__all__'
指定固定欄位: filter_fields = ['name', ...] # 提示: 可以元組, 也可以是列表

2. 程式碼例項#

Copy
from rest_framework.response import Response
from rest_framework.generics import ListAPIView


class TextView6(ListAPIView):
    # 區域性將全域性可能配置的認證+許可權+頻率禁用
    authentication_classes = []
    permission_classes = []
    throttle_classes = []

    queryset = models.Book.objects.all()
    serializer_class = BookModelSerializer

    # filter_fields = ('name', 'price')
filter_fields = '__all__' # 宣告所有的欄位 # url位址列中輸入: http://127.0.0.1:8000/books2/?name=egon

3. 總結#

Copy
安裝 -> 註冊 -> 全域性 or 區域性

4. 注意#

Copy
django-filter的安裝可能會出現django版本最低要求問題,  如果下載最新版本的django-filter
如果使用的是django 1.11版本會自動升級到3.x.

二. 排序元件#

1. 全域性配置 區域性配置#

Copy
# 全域性配置
    # 排序
    REST_FRAMEWORK = {
            'DEFAULT_FILTER_BACKENDS'
: ('rest_framework.filters.OrderingFilter') } # 過濾 和 排序 REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.OrderingFilter') } # 區域性配置 # 排序 from rest_framework.filters import OrderingFilter filter_backends = [OrderingFilter] # 注意: 如果這樣就會覆蓋全域性配置配置的過濾 # 過濾 和 排序 from rest_framework.filters import OrderingFilter from django_filters.rest_framework import DjangoFilterBackend filter_backends = [OrderingFilter, DjangoFilterBackend] filter_fields = '__all__'

2. 程式碼例項#

Copy
class TextView7(ListAPIView):
    # 區域性將全域性可能配置的認證+許可權+頻率禁用
    authentication_classes = []
    permission_classes = []
    throttle_classes = []

    # 區域性配置排序元件.
    # 注意: 如果要過濾和排序, 需要注意的是如果全域性配置了過濾, 需要在宣告排序的基礎之上再什麼過濾. 因為filter_backends的區域性指定會覆蓋過濾的配置.
    # filter_backends = [OrderingFilter, DjangoFilterBackend, ]  # 提示: 2者之間沒有順序
    filter_backends = [DjangoFilterBackend, OrderingFilter]

    queryset = models.Book.objects.all()
    serializer_class = BookModelSerializer

    filter_fields = ['name', 'price']  # 可以用列表, 也可以用元組

3. 總結#

Copy
# 過濾匯入
from django_filters.rest_framework import DjangoFilterBackend
# 排序匯入
from rest_framework.filters import OrderingFilter
# 注意問題
它們2個全域性配置都是共用一個配置路徑, 如果區域性指定了就會將全域性配置的對應項所有的覆蓋

三. 分頁元件#

1. 分頁的三種方式#

Copy
from rest_framework.pagination import PageNumberPagination


# 第一種分頁方式: 通過指定page獲取頁數, 通過size獲取每頁顯示的條目
class CoustomPageNumberPagination(PageNumberPagination):
    """
    url位址列目支援的查詢格式:
    http://api.example.org/accounts/?page=4
    http://api.example.org/accounts/?page=4&page_size=100
    """
    page_size = 3  # 頁面大小. 表示顯示每頁資料條數. 可配置全域性('PAGE_SIZE': None)
    page_query_param = 'page'  # 頁面查詢引數. 表示查詢第幾頁的key. (可自定義名稱)
    page_size_query_param = 'size'  # 頁面大小查詢引數. 表示每一頁顯示的條數 (可自定義名稱)
    max_page_size = 5  # 最大頁面大小. 表示每頁最大顯示條數


from rest_framework.pagination import LimitOffsetPagination


# 第二種分頁方式: 通過指定offset找到指定位置, 通過limit往後獲取條目數
class CustomLimitOffsetPagination(LimitOffsetPagination):
    """
    url位址列目支援的查詢格式:
    http://api.example.org/accounts/?limit=100
    http://api.example.org/accounts/?offset=400&limit=100
    """
    default_limit = 3  # 每頁條數. 可配置全域性('PAGE_SIZE': None,)
    limit_query_param = 'limit'  # 從offset標杆的位置, 往後獲取的條目數 (可自定義名稱)
    offset_query_param = 'offset'  # 標杆. 可以理解為旗幟, 插在這裡, 後去旗幟後面的內容條目 (可自定義名稱)
    max_limit = 5  # 每頁顯示的最大條目數. 預設為None, 不做任何限制.


from rest_framework.pagination import CursorPagination


# 第三種分頁方式: 游標分頁
class CustomCursorPagination(CursorPagination):
    """
    提示: 這種方式實現的方式很複雜, 因此帶來的好處就是查詢效率極高, 不過也有它的缺陷, 就是通過這種方式製作的分頁只有上一頁下一頁,
        如果資料庫資料非常大那麼使用它將會是非常好的選擇.
    url位址列目支援的查詢格式:
        直接瀏覽器輸入訪問即可, 因為不能指定位置.
    """
    cursor_query_param = 'cursor'  # 每一頁查詢的key (可自定義名稱)
    page_size = 3  # 每頁顯示的條數. 可配置全域性('PAGE_SIZE': None,)
    ordering = '-id'  # 排序欄位. 如果不指定預設使用'-created'進行分頁, 如果你的資料庫中沒有這個欄位, 那麼就會丟擲異常


from rest_framework.generics import ListAPIView
from . import ser
from . import models


class BookAPIView(ListAPIView):
    queryset = models.Book.objects.all()
    serializer_class = ser.BookModelSerializer

    """
    APIView -> api_settings -> DEFAULTS -> 51行
    # Generic view behavior
    'DEFAULT_PAGINATION_CLASS': None,
    """
    # pagination_class = CoustomPageNumberPagination
    # pagination_class = CustomLimitOffsetPagination
    pagination_class = CustomCursorPagination

# 總結
    from rest_framework.pagination import PageNumberPagination
        通過page分頁, 通過site獲取分頁條目數
    from rest_framework.pagination import LimitOffsetPagination
        通過offset找到分頁位置, 通過limit獲取當前資料之後的條目數
    from rest_framework.pagination import CursorPagination
        沒有引數, 預設只有上一頁, 下一頁. 通過ordering排序進行分頁
        優點: 查詢速度快. 缺點: 無法選擇起始位置
    提示: 以上都可以限制最大顯示條目數 和 預設獲取條目數. 以及篩選的key都可以重定義.

2. 如果規範化分頁器的restful返回規範#

Copy
from rest_framework.generics import ListAPIView
from utils.response import CommonResponse
from utils.throttle import CustomSimpleRateThrottle
from . import ser
from . import models


class BookAPIView(ListAPIView):
    queryset = models.Book.objects.all()
    serializer_class = ser.BookModelSerializer
    throttle_classes = [CustomSimpleRateThrottle, ]

    def get(self, request, *args, **kwargs):
        instance = self.get_queryset()

        # 呼叫分頁器, 例項化出分頁器物件
        page_obj = CustomCursorPagination()
        instance = page_obj.paginate_queryset(queryset=instance, request=request, view=self)
        # 通過分頁器物件. 獲取下一條的url連結
        next_url = page_obj.get_next_link()
        # 通過分頁器物件. 獲取上一條的url連結
        previous_url = page_obj.get_previous_link()
        # print('next_url:', next_url)
        # print('previous_url:', previous_url)

        serializer = self.get_serializer(instance=instance, many=True)

        return CommonResponse(results=serializer.data, next=next_url, previous_url=previous_url)

# 總結
    # 1. 呼叫分頁器, 例項化出分頁器物件
    page_obj = CustomCursorPagination()
    # 2. 通過分頁器物件. 獲取下一條的url連結  (注意: 一下獲取都在序列化之後)
    next_url = page_obj.get_next_link()
    # 3. 通過分頁器物件. 獲取上一條的url連結
    previous_url = page_obj.get_previous_link()

# 補充: 通過自定義類繼承分頁類, 就可以實現修改引數了, 如果像上面一樣那麼就不能自定義一些引數了
    default_limit = 3
    limit_query_param = 'limit'
    offset_query_param = 'offset'
    max_limit = 5

四. 異常處理#

1. 從原始碼分析到如何實現自定義異常處理#

Copy
# 思路: 發現有些錯誤被drf捕獲了, 而有些錯誤會交給django自己處理, 這是為什麼呢? 原始碼分析一波

# 查詢路徑: APIView -> dispatch -> try..except -> handle_exception

# 原始碼分析:
    try:
        self.initial(request, *args, **kwargs)

        # Get the appropriate handler method
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(),
                              self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed

        response = handler(request, *args, **kwargs)

    except Exception as exc:
        response = self.handle_exception(exc)   # 這裡

    1. 異常的捕獲範圍: 注意, 並不是所有位置的異常出可以捕獲. 例如: 自定義檢視中的類中丟擲的異常就不行
        self.initial(request, *args, **kwargs)
            認證: self.perform_authentication(request)
                提示: 不會捕獲自定義的認證類. 因此perform_authentication做的事情就是request.user賦值
            許可權: self.check_permissions(request)
            頻率: self.check_throttles(request)
            自定義檢視類中的方法:
                response = handler(request, *args, **kwargs)
    2. 關鍵實現 handle_exception方法
        def handle_exception(self, exc):
            # 1) 這裡的在認證失敗的時候會走
            if isinstance(exc, (exceptions.NotAuthenticated,
                                exceptions.AuthenticationFailed)):
                # WWW-Authenticate header for 401 responses, else coerce to 403
                auth_header = self.get_authenticate_header(self.request)

                if auth_header:
                    exc.auth_header = auth_header
                else:
                    exc.status_code = status.HTTP_403_FORBIDDEN

            # 2) 這裡就是通過配置檔案配置的路徑, 拿到處理異常的函式, 內部就一句程式碼: self.settings.EXCEPTION_HANDLER
            '''
            def get_exception_handler(self):
                """
                配置檔案匯入的內容:
                'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
                """
                return self.settings.EXCEPTION_HANDLER
            '''
            exception_handler = self.get_exception_handler()

            # 3) 獲取異常的處理的上下文內容, 本質裡面就是獲取操作的檢視物件的結果
            '''
            def get_exception_handler_context(self):
                return {
                    'view': self,
                    'args': getattr(self, 'args', ()),
                    'kwargs': getattr(self, 'kwargs', {}),
                    'request': getattr(self, 'request', None)
                }
            '''
            context = self.get_exception_handler_context()

            # 4) 將剛剛從配置檔案中匯入的檢視函式傳參呼叫
            '''
            exc: 這裡的exc是APIView中定義的dispatch中傳過來的異常物件
            context: 這裡的context是對出現異常物件的上下文捕獲
            '''
            response = exception_handler(exc, context)

            # 5) 關鍵轉折:
            '''
            這裡就是通過在drf提供的exception_handler函式處理的返回值結果來判斷時候交給django自己處理.
            如果response的返回值是None就會交給django處理了, 現在我們要的就是在exception_handler函式執行完畢以後將返回值進行判斷,
            並且返回的結果不再是None, 而應該是response物件
            '''
            if response is None:
                self.raise_uncaught_exception(exc)

            response.exception = True
            return response

# 步驟:
    1. 先新建一個.py檔案存放自定義的異常處理函式
    2. 在drf提供的預設配置檔案中匯入exception_handler函式的
    3. 在自定義異常處理函式中先將exception_handler傳入讓drf先處理一番, 根據返回的結果為None是來執行自己的判斷.
        如果返回不為None也不應該直接將原本的response物件直接返回, 可以自己封裝一個符合restful規範的類用來繼承Response類
        將原本的response物件中的返回結果通過 response.data.get('detail') 方法獲取
    4. settings.py檔案中配置自定義的exception_handler函式的路徑
        'EXCEPTION_HANDLER': 'app01.app_auth.custom_exception_handler',

2. 程式碼例項#

Copy
from rest_framework.views import exception_handler
from rest_framework import status

from rest_framework.response import Response


class APIResponse(Response):
    def __init__(self, code=1000, messages='成功', results=None, error=None,
                 status=None,
                 template_name=None, headers=None,
                 exception=False, content_type=None, **kwargs):
        data = {
            'code:': code,
            'messages:': messages,
        }
        print('error:', error)
        print('results:', results)
        if results:
            data['results'] = results
        if error:
            data['error'] = error
        data.update(kwargs)

        super().__init__(data=data, status=status,
                         template_name=template_name, headers=headers,
                         exception=exception, content_type=content_type)


def custom_exception_handler(exc, context):
    """
    :param exc: 這裡的exc是APIView中定義的dispatch中傳過來的異常物件
        try:
            ...
        except Exception as exc:
            response = self.handle_exception(exc)
    :param context: 這裡的context是對出現異常物件的上下文捕獲
        查詢: handle_exception -> get_exception_handler_context
            def get_exception_handler_context(self):
                return {
                    'view': self,
                    'args': getattr(self, 'args', ()),
                    'kwargs': getattr(self, 'kwargs', {}),
                    'request': getattr(self, 'request', None)
                }
    :return: 這裡返回Response物件, 本來drf沒有處理的的異常會交給django處理, 但是我們捕獲這種異常, 規定成統一的處理. 讓drf處理.

    注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    這裡可以捕獲的異常範圍由以下原始碼得知:
        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)
        except Exception as exc:
            response = self.handle_exception(exc)

    捕獲範圍:
        self.initial(request, *args, **kwargs)
            認證: self.perform_authentication(request)
                提示: 不會捕獲自定義的認證類. 因此perform_authentication做的事情就是request.user賦值
            許可權: self.check_permissions(request)
            頻率: self.check_throttles(request)
            自定義檢視類中的方法:
                response = handler(request, *args, **kwargs)
    """
    obj = None
    response = exception_handler(exc, context)
    # 注意: exc, context都不是可json序列化的格式, 需要轉換成字串型別.
    if not response:
        # 自己的處理
        if isinstance(exc, AttributeError):
            obj = APIResponse(2000, '失敗', error=str(exc), results=str(context), status=status.HTTP_403_FORBIDDEN)
        elif isinstance(exc, ImportError):
            obj = APIResponse(2002, '失敗', error=str(exc), results=str(context), status=status.HTTP_403_FORBIDDEN)
        elif isinstance(exc, TypeError):
            obj = APIResponse(2003, '失敗', error=str(exc), results=str(context), status=status.HTTP_403_FORBIDDEN)
        elif isinstance(exc, Exception):
            obj = APIResponse(2004, '失敗', error=str(exc), results=str(context), status=status.HTTP_403_FORBIDDEN)
    else:
        # 在drf處理的基礎之上再次處理
        obj = APIResponse(2005, '失敗', error=response.data.get('detail'), results=str(context),
                          status=status.HTTP_403_FORBIDDEN)
    return obj

3. 總結#

Copy
1. 匯入需要在drf提供的預設函式的基礎之上的函式
	from rest_framework.views import exception_handler
2. 自定義異常處理函式2個引數exc, context
3. 先讓drf處理一波, 處理它處理不完的, 或者 在他處理完的基礎之上拓展, 通過response返回結果來進行區分
    提示: 可以通過 response.data.get('detail') 獲取drf處理完的物件中返回的響應資訊

4. 注意#

Copy
配置檔案中配置自定義的異常處理函式時, drf提供的exception_handler的匯入會與在同一個檔案中自定義的認證類 或者 自定義的許可權類的匯入起衝突.
自定義的異常的處理程式碼邏輯最好新建一個純淨的.py檔案存放

5. 快速使用#

Copy
from rest_framework.views import exception_handler
from rest_framework.response import Response


class CommonResponse(Response):
    def __init__(self, code=1000, messages='ok', results=None,
                 status=None, template_name=None, headers=None,
                 exception=False, content_type=None,
                 **kwargs):
        data = {
            'code': code,
            'messages': messages,
        }
        data.update(kwargs)
        if results:
            data['results'] = results
        super().__init__(data=data, status=status,
                         template_name=template_name, headers=headers,
                         exception=exception, content_type=content_type)



def common_exception_handler(exc, context):
    response = exception_handler(exc, context)
    if not response:
        obj = CommonResponse(code=2000, messages='error', results=str(exc))
    else:
        obj = CommonResponse(code=2000, messages='error', results=response.data)
    return obj