DRF之分頁類原始碼分析

ssrheart發表於2024-04-23

DRF之分頁類原始碼分析

【一】分頁類介紹

  • Django REST framework(DRF)是一個用於構建Web API的強大工具,它提供了分頁功能,使你能夠控制API響應的資料量。
  • 在DRF中,分頁功能由分頁類(Paginator Class)來管理。

【二】內建分頁類

  • 在DRF中,分頁類通常位於rest_framework.pagination模組中,它們用於分割長列表或查詢集,以便在API響應中只返回一部分資料。以下是一些常見的DRF分頁類:
    • PageNumberPagination:這是最常見的分頁類,它使用頁碼來分割資料。
    • LimitOffsetPagination:這種分頁類使用限制和偏移量來分頁,允許你指定返回的結果數量和從哪裡開始。
    • CursorPagination:這是一種基於遊標的分頁,適用於需要深度分頁的情況,如社交媒體應用。
    • CustomPagination:你還可以自定義自己的分頁類,以滿足特定需求。

【三】分頁類的執行流程

  • 請求到達DRF檢視:
    • 當一個API請求到達DRF檢視時,DRF檢視會根據檢視的配置和查詢引數來選擇使用哪個分頁類。
    • 通常,你可以在檢視類中設定pagination_class屬性來指定使用的分頁類。
  • 例項化分頁類:
    • 一旦確定了要使用的分頁類,DRF將例項化該分頁類的物件。
    • 這個物件將在後續的處理中負責執行分頁操作。
  • 查詢資料:
    • 檢視從資料庫或其他資料來源查詢資料,並將資料傳遞給分頁類的例項。
  • 分頁資料:
    • 分頁類根據查詢引數(如頁碼、每頁數量等)對資料進行分頁,並返回一個包含分頁結果的序列化物件。
  • 構建API響應:
    • 檢視將包含分頁結果的序列化物件新增到API響應中,並返回給客戶端

【四】基礎分頁

class BasePagination:
    display_page_controls = False

    def paginate_queryset(self, queryset, request, view=None):  # pragma: no cover
        raise NotImplementedError('paginate_queryset() must be implemented.')

    def get_paginated_response(self, data):  # pragma: no cover
        raise NotImplementedError('get_paginated_response() must be implemented.')

    def get_paginated_response_schema(self, schema):
        return schema

    def to_html(self):  # pragma: no cover
        raise NotImplementedError('to_html() must be implemented to display page controls.')

    def get_results(self, data):
        return data['results']

    def get_schema_fields(self, view):
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        return []

    def get_schema_operation_parameters(self, view):
        return []

【五】基本分頁PageNumberPagination

  • PageNumberPagination:這是最常見的分頁類,它使用頁碼來分割資料。

【1】使用

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


class BookNumberPagination(PageNumberPagination):
    # 重寫 4 個 類屬性
    page_size = 2  # 每頁顯示的條數

    page_query_param = 'page'  # 路徑後面的 引數:page=4(第4頁)

    page_size_query_param = 'page_size'  # page=4&page_size=5:查詢第4頁,每頁顯示5條

    max_page_size = 5  # 每頁最多顯示5條
  • 自定義了一個名為BookNumberPagination的分頁類
    • 繼承自PageNumberPagination
  • 類屬性說明:
    • page_size:每頁顯示的條數,預設值為2條。
    • page_query_param:路徑後面指定的引數名,預設為page
      • 例如,http://127.0.0.1:8000/app01/v1/books/?page=4表示查詢第4頁的資料。
    • page_size_query_param:路徑後面指定的引數名,表示每頁顯示的條數,預設為page_size
      • 例如,http://127.0.0.1:8000/app01/v1/books/?page=4&page_size=5表示查詢第4頁的資料,每頁顯示5條。
    • max_page_size:每頁最多顯示的條數,預設值為5條。
  • 返回結果說明:
    • count:符合查詢條件的總記錄數,即所有記錄的數量。
    • next:下一頁的URL連結,如果有下一頁資料,則返回對應的URL;否則返回null。
    • previous:上一頁的URL連結,如果有上一頁資料,則返回對應的URL;否則返回null。
    • results:當前頁的資料列表。

【2】原始碼分析

class PageNumberPagination(BasePagination):
    """
    A simple page number based style that supports page numbers as
    query parameters. For example:

    http://api.example.org/accounts/?page=4
    http://api.example.org/accounts/?page=4&page_size=100
    """
    # The default page size.
    # Defaults to `None`, meaning pagination is disabled.
    
    # 預設每頁的數量。如果沒有設定page_size_query_param,則預設為此值。
    page_size = api_settings.PAGE_SIZE
	
    # 用於分頁的Django分頁器類。預設是DjangoPaginator,它用於根據page_size將查詢集分頁。
    django_paginator_class = DjangoPaginator

    # Client can control the page using this query parameter.
    
    # 客戶端可以使用的查詢引數來控制頁碼。預設是'page'
    page_query_param = 'page'
    
    # 頁碼查詢引數的描述
    page_query_description = _('A page number within the paginated result set.')

    # Client can control the page size using this query parameter.
    # Default is 'None'. Set to eg 'page_size' to enable usage.
    
    # 客戶端可以使用的查詢引數來控制每頁的數量。預設是None,表示不啟用此功能。
    page_size_query_param = None
    
    # 每頁數量查詢引數的描述
    page_size_query_description = _('Number of results to return per page.')

    # Set to an integer to limit the maximum page size the client may request.
    # Only relevant if 'page_size_query_param' has also been set.
    
    # 用於限制客戶端可請求的最大每頁數量的整數。僅在page_size_query_param已啟用時有效。
    max_page_size = None
	
    # 字串列表,表示最後一頁的字串描述。預設為('last',)。
    last_page_strings = ('last',)
	
    # 分頁HTML模板的路徑,預設為'rest_framework/pagination/numbers.html'
    template = 'rest_framework/pagination/numbers.html'
	
    # 無效頁碼時的錯誤訊息
    invalid_page_message = _('Invalid page.')
	
    # 在檢視中執行分頁操作。
    # queryset(查詢集),request(請求物件),和 view(檢視物件)。這些引數是用於執行分頁操作所需的基本資訊
    def paginate_queryset(self, queryset, request, view=None):
        """
        Paginate a queryset if required, either returning a
        page object, or `None` if pagination is not configured for this view.
        """
        
        # 首先獲取每頁的數量(page_size),透過呼叫 self.get_page_size(request) 方法來獲取。
        page_size = self.get_page_size(request)
        # 如果 page_size 為 None,表示分頁未配置,函式將返回 None,即不進行分頁操作
        if not page_size:
            return None
		
        # 建立了一個Django分頁器(paginator)物件,使用傳入的 queryset 和 page_size 引數。
        # 這將根據查詢集的大小和每頁的數量建立分頁。
        paginator = self.django_paginator_class(queryset, page_size)
        
        # 獲取當前請求的頁碼(page_number),透過呼叫 self.get_page_number(request, paginator) 方法來獲取。
        # 如果請求中的頁碼是 'last',則頁碼將設定為最後一頁的頁碼。
        page_number = self.get_page_number(request, paginator)

        try:
            # 嘗試使用分頁器將查詢集分頁,即執行實際的分頁操作,透過呼叫 paginator.page(page_number) 方法。
            # 如果分頁操作成功,函式將分頁後的頁面物件(self.page)儲存下來,以備後續使用。
            self.page = paginator.page(page_number)
        except InvalidPage as exc:
            # 如果頁碼無效(例如,超出了分頁範圍),則會引發 InvalidPage 異常。
            msg = self.invalid_page_message.format(
                page_number=page_number, message=str(exc)
            )
            raise NotFound(msg)
		
        # 如果總頁數大於1且模板(template)已經設定,表示有多頁資料可供分頁
        if paginator.num_pages > 1 and self.template is not None:
            # The browsable API should display pagination controls.
            
            # 於是函式將 display_page_controls 設定為 True,以便在瀏覽API時顯示分頁控制元件。
            self.display_page_controls = True
		
        # 將請求物件儲存在 self.request 中,並返回分頁後的資料列表,即當前頁的資料。
        self.request = request
        
        # 最後,它返回分頁後的資料列表。
        return list(self.page)
	
    # 從請求中獲取頁碼
    def get_page_number(self, request, paginator):
        
        # # 從請求中獲取頁碼
        page_number = request.query_params.get(self.page_query_param, 1)
        
        # 如果頁碼為last_page_strings中的任何一個字串
        if page_number in self.last_page_strings:
            
            # 則返回最後一頁的頁碼。
            page_number = paginator.num_pages
            
        # 否則則返回預設頁碼 1
        return page_number
	
    # 根據分頁後的資料建立響應,包括總數、下一頁和上一頁的連結和當前頁資料
    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('count', self.page.paginator.count),
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))
	
    # 返回用於響應分頁資料的JSON Schema。
    # 返回一個 JSON Schema,用於描述分頁響應的結構。JSON Schema 是一種用於驗證和描述 JSON 資料結構的規範
    def get_paginated_response_schema(self, schema):
        return {
            # 表示根物件是一個 JSON 物件
            'type': 'object',
            
            # 包含不同屬性的字典,描述了響應物件的各個欄位
            'properties': {
                
                 # 表示總記錄數
                'count': {
                    # 其型別為整數('integer')
                    'type': 'integer',
                    # 並提供一個示例值為 123
                    'example': 123,
                },
                
                # 表示下一頁的連結
                'next': {
                    # 其型別為字串('string')
                    'type': 'string',
                    # 此欄位可為空('nullable': True),因為最後一頁沒有下一頁
                    'nullable': True,
                    # 
                    'format': 'uri',
                    # 它還提供了一個示例連結
                    # 包括了 {page_query_param},它將在實際響應中替換為頁碼查詢引數的值。
                    'example': 'http://api.example.org/accounts/?{page_query_param}=4'.format(
                        page_query_param=self.page_query_param)
                },
                # 表示上一頁的連結
                'previous': {
                    # 其型別為字串('string')
                    'type': 'string',
                    # 此欄位也可為空,因為第一頁沒有上一頁
                    'nullable': True,
                    'format': 'uri',
                    # 它同樣提供了一個示例連結,包括了 {page_query_param},將在實際響應中替換為頁碼查詢引數的值
                    'example': 'http://api.example.org/accounts/?{page_query_param}=2'.format(
                        page_query_param=self.page_query_param)
                },
                
                # 表示分頁後的資料結果,它的結構由傳入的 schema 引數決定。
                # 這個欄位沒有提供示例值,因為它的結構取決於實際的資料模型
                'results': schema,
            },
        }
	
    # 從請求中獲取每頁的數量。
    def get_page_size(self, request):
        # 檢查是否啟用了 page_size_query_param(即客戶端可以透過查詢引數來控制每頁的數量)。
        # 如果 page_size_query_param 已啟用,則進入以下步驟
        if self.page_size_query_param:
            try:
                # 嘗試從請求的查詢引數中獲取每頁的數量。
                # 具體來說,它使用 request.query_params 字典來查詢與 page_size_query_param 對應的查詢引數值。
                # 這裡使用了 request.query_params 是因為查詢引數通常包含在請求的 URL 中
                # 如果成功獲取查詢引數的值,函式嘗試將其轉換為正整數(_positive_int)。這是因為頁碼數量必須是正整數。
                return _positive_int(
                    request.query_params[self.page_size_query_param],
                    # # 如果成功轉換為正整數,則返回該值作為每頁的數量,並且啟用了嚴格模式(strict=True)。
                    strict=True,
                    # # 同時,還應用了 cutoff=self.max_page_size
                    # 這表示如果超出了 self.max_page_size 指定的最大頁碼數量,則會被截斷為最大值。
                    cutoff=self.max_page_size
                )
            # 如果轉換失敗(例如,查詢引數不存在或不是整數),則會捕獲 KeyError 和 ValueError 異常。
            except (KeyError, ValueError):
                pass
		
        # 否則使用預設的 self.page_size。
        return self.page_size
	
    # 獲取下一頁的連結。這些連結在響應中提供客戶端導航。
    def get_next_link(self):
        # 首先,函式檢查當前頁是否有下一頁,透過呼叫 self.page.has_next() 來判斷。
        if not self.page.has_next():
            # 如果沒有下一頁,則直接返回 None。
            return None
        
        # 如果當前頁有下一頁,函式獲取當前請求的絕對URL,透過 self.request.build_absolute_uri() 方法獲取
        url = self.request.build_absolute_uri()
        
        # 獲取下一頁的頁碼,透過呼叫 self.page.next_page_number() 來獲取。這個方法返回下一頁的頁碼
        page_number = self.page.next_page_number()
        
        # 使用 replace_query_param 方法,將當前頁的頁碼查詢引數替換為下一頁的頁碼,以生成下一頁的連結。
        # 這個連結將用於導航到下一頁的資料
        return replace_query_param(url, self.page_query_param, page_number)
	
    # 獲取上一頁的連結。這些連結在響應中提供客戶端導航。
    def get_previous_link(self):
        # 獲取上一頁的連結,以便在分頁響應中提供給客戶端進行導航。
        # 檢查當前頁是否有上一頁,透過呼叫 self.page.has_previous() 來判斷。
        if not self.page.has_previous():
            # 如果當前頁沒有上一頁,則返回 None。
            return None
        
        
        # 如果當前頁有上一頁,函式獲取當前請求的絕對URL,透過 self.request.build_absolute_uri() 方法獲取。
        url = self.request.build_absolute_uri()
        
        # 獲取上一頁的頁碼,透過呼叫 self.page.previous_page_number() 來獲取。這個方法返回上一頁的頁碼。
        page_number = self.page.previous_page_number()
        
        # 如果上一頁的頁碼是1,表示上一頁就是第一頁
        if page_number == 1:
            # 使用 remove_query_param 方法去除查詢引數中的頁碼查詢引數,以生成上一頁的連結。
            return remove_query_param(url, self.page_query_param)
        
        # 如果上一頁的頁碼不是1,使用 replace_query_param 方法,將當前頁的頁碼查詢引數替換為上一頁的頁碼,以生成上一頁的連結。
        # 這個連結將用於導航到上一頁的資料。
        return replace_query_param(url, self.page_query_param, page_number)
	
    # 獲取用於HTML渲染的上下文資訊。它構建一個包含上一頁連結、下一頁連結和頁碼連結的字典,並返回這個字典
    def get_html_context(self):
        # 獲取當前請求的絕對URL,透過 self.request.build_absolute_uri() 方法獲取。
        base_url = self.request.build_absolute_uri()
		
        # 定義了一個巢狀函式 page_number_to_url,用於將頁碼對映到相應的URL。
        def page_number_to_url(page_number):
            # 如果頁碼是1,表示當前頁是第一頁
            if page_number == 1:
                # 呼叫 remove_query_param 方法去除查詢引數中的頁碼查詢引數,生成上一頁的URL
                return remove_query_param(base_url, self.page_query_param)
            else:
                # 否則,呼叫 replace_query_param 方法將當前頁的頁碼查詢引數替換為新的頁碼,生成頁碼連結。
                return replace_query_param(base_url, self.page_query_param, page_number)
		
        # 獲取當前頁碼和最後一頁的頁碼。這兩個值用於生成頁碼連結
        current = self.page.number
        final = self.page.paginator.num_pages
        
        # 使用 _get_displayed_page_numbers 函式來生成要顯示的頁碼列表,這個列表通常包括當前頁及其周圍的幾個頁碼。
        page_numbers = _get_displayed_page_numbers(current, final)
        # 當前頁連結
        page_links = _get_page_links(page_numbers, current, page_number_to_url)
		
        # 函式返回包含上一頁URL、下一頁URL和頁碼連結列表的字典
        return {
            # 上一頁 URL 
            'previous_url': self.get_previous_link(),
            # 下一頁 URL
            'next_url': self.get_next_link(),
            # 當前頁連結
            'page_links': page_links
        }
	
    # 將分頁結果渲染成HTML格式。
    def to_html(self):
        # 獲取HTML模板,模板路徑由 self.template 指定
        template = loader.get_template(self.template)
        # 呼叫 get_html_context 獲取HTML渲染所需的上下文資訊
        context = self.get_html_context()
        # 使用模板引擎渲染模板並傳遞上下文資訊,返回渲染後的HTML內容
        return template.render(context)
	
    # 生成用於API Schema的欄位描述。它返回一個包含查詢引數欄位的列表,用於描述分頁請求的Schema
    def get_schema_fields(self, view):
        
        # 檢查是否安裝了 coreapi 和 coreschema,這些是用於生成API Schema的庫。
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
        
        # 建立一個 coreapi.Field 物件,用於描述頁碼查詢引數欄位。
        fields = [
            # 它包括
            coreapi.Field(
                # 欄位的名稱
                name=self.page_query_param,
                # 欄位是否必須
                required=False,
                # 欄位的位置(query)
                location='query',
                # 欄位的型別(integer)
                schema=coreschema.Integer(
                    # 欄位的標題
                    title='Page',
                    # 欄位的描述資訊
                    description=force_str(self.page_query_description)
                )
            )
        ]
        
        # 如果分頁類還支援頁碼大小查詢引數(self.page_size_query_param 不為 None)
        if self.page_size_query_param is not None:
            
            # 建立一個額外的 coreapi.Field 物件,用於描述頁碼大小查詢引數欄位。
            fields.append(
                coreapi.Field(
                    name=self.page_size_query_param,
                    required=False,
                    location='query',
                    schema=coreschema.Integer(
                        title='Page size',
                        description=force_str(self.page_size_query_description)
                    )
                )
            )
            
        # 返回包含欄位描述的列表
        return fields
	
    # 用於生成API操作的引數描述。它返回一個包含操作引數描述的列表,用於描述分頁請求的引數
    def get_schema_operation_parameters(self, view):
        
        # 建立一個引數字典,包括引數的名稱、是否必需、位置(query)、描述等資訊。這個字典描述了頁碼查詢引數
        parameters = [
            {
                'name': self.page_query_param,
                'required': False,
                'in': 'query',
                'description': force_str(self.page_query_description),
                'schema': {
                    'type': 'integer',
                },
            },
        ]
        # 如果分頁類還支援頁碼大小查詢引數(self.page_size_query_param 不為 None)
        if self.page_size_query_param is not None:
            # # 建立一個額外的引數字典,用於描述頁碼大小查詢引數。
            parameters.append(
                {
                    'name': self.page_size_query_param,
                    'required': False,
                    'in': 'query',
                    'description': force_str(self.page_size_query_description),
                    'schema': {
                        'type': 'integer',
                    },
                },
            )
        # 返回包含引數描述的列表
        return parameters

【六】偏移分頁LimitOffsetPagination

  • LimitOffsetPagination:這種分頁類使用限制和偏移量來分頁,允許你指定返回的結果數量和從哪裡開始。

【1】使用

# (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  # 限制每次取的最大條數
  • 自定義分頁類:
    • 程式碼中定義了一個自定義的分頁類BookLimitOffsetPagination
    • 它繼承自LimitOffsetPagination
  • 在這個類中我們可以重寫四個類屬性來設定分頁的相關引數:
    • default_limit:每頁顯示的條數,預設值為2。
    • limit_query_param:用於指定每頁取多少條資料的查詢引數,預設為limit
    • offset_query_param:用於指定偏移量的查詢引數,預設為offset
      • 透過設定這個引數,可以使得分頁結果實現偏移取值
      • 即從第幾條資料開始取,然後取多少條資料。
    • max_limit:限制每次獲取的最大條數,預設值為5。

【2】原始碼分析

class LimitOffsetPagination(BasePagination):
    """
    A limit/offset based style. For example:

    http://api.example.org/accounts/?limit=100
    http://api.example.org/accounts/?offset=400&limit=100
    """
    
    # 預設每頁返回的數量,預設值為 api_settings.PAGE_SIZE,通常是 API 的預設頁大小。
    default_limit = api_settings.PAGE_SIZE
    
    # 用於客戶端設定每頁數量的查詢引數名稱,預設為 'limit'。
    limit_query_param = 'limit'
    # 查詢引數的描述,預設為 Number of results to return per page.。
    limit_query_description = _('Number of results to return per page.')
    # 用於客戶端設定偏移量的查詢引數名稱,預設為 'offset'
    offset_query_param = 'offset'
    # 查詢引數的描述,預設為 'The initial index from which to return the results.'
    offset_query_description = _('The initial index from which to return the results.')
    # 用於限制客戶端可以請求的最大每頁數量,預設為 None,表示沒有最大限制。
    max_limit = None
    #  用於HTML渲染的模板路徑,預設為 'rest_framework/pagination/numbers.html'。
    template = 'rest_framework/pagination/numbers.html'
	
    # 分頁查詢集。它接收查詢集、請求物件和檢視物件作為引數,執行以下邏輯:
    def paginate_queryset(self, queryset, request, view=None):
        # 獲取 limit 和 offset,透過 self.get_limit(request) 和 self.get_offset(request) 方法。
        self.limit = self.get_limit(request)
        if self.limit is None:
            return None
		
        # 獲取查詢集的總數量 count,透過 self.get_count(queryset) 方法。
        self.count = self.get_count(queryset)
        # 
        self.offset = self.get_offset(request)
        self.request = request
        
        # 如果 count 大於 limit 且定義了模板路徑,則標記顯示分頁控制元件
        if self.count > self.limit and self.template is not None:
            self.display_page_controls = True
		
        if self.count == 0 or self.offset > self.count:
            return []
        
        # # 否則,返回從查詢集中獲取的 offset 到 offset + limit 範圍內的資料列表
        return list(queryset[self.offset:self.offset + self.limit])
	
    # 這個函式返回用於響應分頁資料的字典
    # 包括 count(總數)、next(下一頁連結)、previous(上一頁連結)和 results(當前頁的資料)。
    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('count', self.count),
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))
	
    # 返回一個JSON格式的分頁響應模板,該模板描述了分頁響應的結構
    def get_paginated_response_schema(self, schema):
        return {
            'type': 'object',
            'properties': {
                # count: 表示結果總數的整數
                'count': {
                    'type': 'integer',
                    'example': 123,
                },
                # next: 表示下一頁的URI(統一資源識別符號)
                'next': {
                    # 是一個字串
                    'type': 'string',
                    # 可以為null(可為空)
                    'nullable': True,
                    'format': 'uri',
                    # 該欄位描述了下一頁的URL,其中包括了分頁查詢的引數,如offset_param和limit_param。
                    'example': 'http://api.example.org/accounts/?{offset_param}=400&{limit_param}=100'.format(
                        offset_param=self.offset_query_param, limit_param=self.limit_query_param),
                },
                # previous: 表示上一頁的URI
                'previous': {
                    # 也是一個字串
                    'type': 'string',
                    # 可以為null
                    'nullable': True,
                    'format': 'uri',
                    # 類似於next欄位,描述了上一頁的URL,包括分頁查詢引數
                    'example': 'http://api.example.org/accounts/?{offset_param}=200&{limit_param}=100'.format(
                        offset_param=self.offset_query_param, limit_param=self.limit_query_param),
                },
                # results: 表示包含實際結果資料的欄位。這個欄位的結構由引數schema定義,它應該是一個包含實際資料結構的JSON物件。
                'results': schema,
            },
        }
	
    # 該方法用於獲取分頁查詢中的限制引數(即每頁返回的結果數量)
    def get_limit(self, request):
        
        # 檢查請求中是否包含了limit_query_param指定的引數(通常是limit)
        # 如果存在並且是一個正整數,則返回該值
        if self.limit_query_param:
            try:
                return _positive_int(
                    
                    request.query_params[self.limit_query_param],
                    # # 引數strict=True表示要求限制引數是正整數
                    strict=True,
                    # cutoff=self.max_limit表示限制引數不能超過max_limit的值。
                    cutoff=self.max_limit
                )
            except (KeyError, ValueError):
                pass
		
        # 否則,返回預設值default_limit
        return self.default_limit
	
    # 該方法用於獲取分頁查詢中的偏移引數(即從哪裡開始返回結果)。
    def get_offset(self, request):
        try:
            # 從請求中獲取offset_query_param指定的引數(通常是offset)
            # 它嘗試,如果存在並且是一個正整數,則返回該值
            return _positive_int(
                request.query_params[self.offset_query_param],
            )
        except (KeyError, ValueError):
            # 否則,返回0作為預設值。
            return 0
	
    # 該方法用於生成下一頁的連結。
    def get_next_link(self):
        # 如果當前頁已經是最後一頁或沒有更多的資料,它將返回None。
        if self.offset + self.limit >= self.count:
            return None
		
        # 否則,它會構建下一頁的URL,並將offset和limit引數更新為下一頁的值
        url = self.request.build_absolute_uri()
        url = replace_query_param(url, self.limit_query_param, self.limit)

        offset = self.offset + self.limit
        # 後返回新的URL。
        return replace_query_param(url, self.offset_query_param, offset)
	
    # 該方法用於生成上一頁的連結。
    def get_previous_link(self):
        
        # 檢查當前頁是否是第一頁或者offset值是否小於等於0。
        # 如果是,說明沒有上一頁,直接返回None表示沒有上一頁連結。
        if self.offset <= 0:
            return None
		
        # 如果當前頁不是第一頁且offset值大於0,那麼就需要構建上一頁的連結。
        # 首先,獲取當前請求的絕對URL地址,這個URL包含了當前頁面的查詢引數。
        url = self.request.build_absolute_uri()
        # 接下來,透過呼叫replace_query_param函式,將當前URL中的limit_query_param引數替換為當前分頁器的limit值。
        # 這是因為上一頁的連結不應該改變每頁的限制數量,只需要更新offset引數。
        url = replace_query_param(url, self.limit_query_param, self.limit)
		
        # 判斷如果offset - limit小於等於0,說明上一頁的起始位置應該是0
        if self.offset - self.limit <= 0:
            # 因此呼叫remove_query_param函式移除offset_query_param引數。
            return remove_query_param(url, self.offset_query_param)
		
        # 計算新的offset值,即offset - limit
        # 並使用replace_query_param函式將URL中的offset_query_param引數替換為新的offset值。
        offset = self.offset - self.limit
        # 否則,它會構建上一頁的URL,將offset和limit引數更新為上一頁的值,然後返回新的URL。
        return replace_query_param(url, self.offset_query_param, offset)
	
    # 構建分頁器在HTML頁面中的顯示。
    def get_html_context(self):
        # 獲取當前請求的絕對URL地址,並儲存在base_url變數中。這個URL包含了當前頁面的查詢引數。
        base_url = self.request.build_absolute_uri()
		
        # 檢查是否設定了limit引數,
        if self.limit:
            # 如果設定了,就計算當前頁碼current和最終頁碼final。
            # 計算當前頁碼的方式是透過將offset除以limit然後加1,因為頁碼通常從1開始。
            # 最終頁碼的計算比較複雜,需要考慮不完全分頁的情況,即offset不是limit的整數倍時,可能會有一個額外的頁面。
            current = _divide_with_ceil(self.offset, self.limit) + 1

            # The number of pages is a little bit fiddly.
            # We need to sum both the number of pages from current offset to end
            # plus the number of pages up to the current offset.
            # When offset is not strictly divisible by the limit then we may
            # end up introducing an extra page as an artifact.
            final = (
                _divide_with_ceil(self.count - self.offset, self.limit) +
                _divide_with_ceil(self.offset, self.limit)
            )
			
            
            final = max(final, 1)
        else:
            current = 1
            final = 1
		
        # 如果當前頁碼current大於最終頁碼final,將current設定為final,以確保當前頁碼不超過最終頁碼
        if current > final:
            current = final
		
        # 定義了一個內部函式page_number_to_url,用於將頁碼轉換為相應的URL連結。
        def page_number_to_url(page_number):
            # 如果頁碼是1
            if page_number == 1:
                # 呼叫remove_query_param函式移除offset_query_param引數,表示回到第一頁。
                return remove_query_param(base_url, self.offset_query_param)
            else:
                # 否則,計算新的offset值
                offset = self.offset + ((page_number - current) * self.limit)
                # 然後呼叫replace_query_param函式將offset_query_param引數替換為新的offset值,以構建包含指定頁碼的URL。
                return replace_query_param(base_url, self.offset_query_param, offset)
		
        # 呼叫_get_displayed_page_numbers函式獲取在HTML頁面中要顯示的頁碼列表page_numbers
        page_numbers = _get_displayed_page_numbers(current, final)
         # 呼叫_get_page_links函式生成頁碼連結列表page_links,傳入當前頁碼、最終頁碼和頁碼轉換函式。
        page_links = _get_page_links(page_numbers, current, page_number_to_url)
		
        # 返回一個包含上一頁URL、下一頁URL和頁碼連結列表的字典,用於HTML渲染分頁資訊。
        return {
            'previous_url': self.get_previous_link(),
            'next_url': self.get_next_link(),
            'page_links': page_links
        }
        
	# 
    def to_html(self):
        # 透過loader.get_template(self.template)獲取到指定模板的模板物件,並儲存在template變數中。
        # 這個模板物件將用於渲染HTML頁面。
        template = loader.get_template(self.template)
        # 呼叫self.get_html_context()方法獲取HTML渲染上下文,這個上下文包含了分頁資訊,包括上一頁URL、下一頁URL和頁碼連結。
        context = self.get_html_context()
        # 使用獲取的模板物件template和上下文context來渲染HTML頁面,並返回渲染後的HTML內容。
        return template.render(context)
	
    # 這個方法主要用於確定總共有多少個物件,通常用於計算分頁資訊中的總記錄數。
    def get_count(self, queryset):
        """
        Determine an object count, supporting either querysets or regular lists.
        """
        try:
            # 接受一個查詢集或普通列表作為引數,然後嘗試使用queryset.count()來獲取物件的數量。
            # 如果無法使用count()方法
            return queryset.count()
        
        # 出現AttributeError或TypeError異常,就會捕獲
        except (AttributeError, TypeError):
            # 嘗試使用len(queryset)來獲取物件的數量
            return len(queryset)

    def get_schema_fields(self, view):
        # assert coreapi is not None 和 assert coreschema is not None 這兩個斷言語句用於檢查是否安裝了coreapi和coreschema庫,因為這兩個庫用於生成API文件。
        # 如果這兩個庫未安裝,將引發AssertionError異常。
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
        
        # 返回一個包含兩個coreapi.Field物件的列表,這兩個物件分別代表了API的兩個請求引數:limit和offset。
        return [
            # coreapi.Field 用於定義API文件中的一個欄位。
            # 在這裡,我們定義了兩個欄位,一個是limit欄位,一個是offset欄位。
            coreapi.Field(
                # name=self.limit_query_param 和 name=self.offset_query_param 分別指定了這兩個欄位的名稱
                # 這些名稱通常對應於API中的查詢引數名稱,例如?limit=10和?offset=20。
                name=self.limit_query_param,
                # required=False 表示這兩個欄位是可選的,客戶端可以選擇是否傳遞它們。
                required=False,
                # location='query' 指定了這兩個欄位的位置是查詢引數。
                location='query',
                # 指定了這兩個欄位的資料型別為整數(Integer),並提供了標題(title)和描述(description)資訊
                # 這些資訊將顯示在API文件中。
                schema=coreschema.Integer(
                    title='Limit',
                    description=force_str(self.limit_query_description)
                )
            ),
            coreapi.Field(
                name=self.offset_query_param,
                required=False,
                location='query',
                schema=coreschema.Integer(
                    title='Offset',
                    description=force_str(self.offset_query_description)
                )
            )
        ]

    def get_schema_operation_parameters(self, view):
        # parameters 是一個列表,其中包含了兩個字典物件,每個字典物件代表一個操作引數。
        parameters = [
            {
                # 'name': 引數的名稱,分別為limit和offset。
                'name': self.limit_query_param,
                # 'required': 引數是否為必需的,這裡設定為False,表示這兩個引數是可選的。
                'required': False,
                # 'in': 引數的位置,這裡設定為query,表示這兩個引數位於請求的查詢引數中。
                'in': 'query',
                # 'description': 引數的描述,透過force_str(self.limit_query_description) 和 force_str(self.offset_query_description) 獲取描述資訊。
                'description': force_str(self.limit_query_description),
                # 'schema': 引數的資料型別和格式的定義。在這裡,'type' 設定為 'integer',表示引數的資料型別是整數。
                'schema': {
                    'type': 'integer',
                },
            },
            {
                'name': self.offset_query_param,
                'required': False,
                'in': 'query',
                'description': force_str(self.offset_query_description),
                'schema': {
                    'type': 'integer',
                },
            },
        ]
        return parameters

【七】遊標分頁CursorPagination

  • CursorPagination:這是一種基於遊標的分頁,適用於需要深度分頁的情況,如社交媒體應用。

【1】使用

# (3)CursorPagination:遊標分頁
# 只能上一頁或下一頁,但是速度特別快,經常用於APP上
class BookCursorPagination(CursorPagination):
    # 重寫3個類屬性
    cursor_query_param = 'cursor'  # 查詢引數
    page_size = 2  # 每頁顯示2條
    ordering = 'id'  # 必須是要分頁的資料表中的欄位,一般是id
  • 定義了一個自定義的分頁類BookCursorPagination,它繼承自Django Rest Framework提供的CursorPagination類。
  • 該分頁類透過設定一些屬性來控制分頁的行為,其中包括:
    • cursor_query_param: 指定查詢引數名,這裡設定為cursor,表示透過該引數來指定遊標位置。
    • page_size: 指定每頁顯示的記錄數,這裡設定為2條。
    • ordering: 指定按照哪個欄位排序進行分頁,這裡設定為id欄位。

【2】原始碼分析

class CursorPagination(BasePagination):
    """
    The cursor pagination implementation is necessarily complex.
    For an overview of the position/offset style we use, see this post:
    https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
    """
    
    # cursor_query_param 和 cursor_query_description:定義了查詢引數名稱和描述,用於表示遊標值。
    cursor_query_param = 'cursor'
    cursor_query_description = _('The pagination cursor value.')
    
    # page_size:定義了每頁的預設大小
    page_size = api_settings.PAGE_SIZE
    
    # invalid_cursor_message:定義了無效遊標的錯誤訊息
    invalid_cursor_message = _('Invalid cursor')
    
    # ordering:定義了預設的排序方式
    ordering = '-created'
    # template:定義了用於呈現分頁控制元件的模板
    template = 'rest_framework/pagination/previous_and_next.html'

    # Client can control the page size using this query parameter.
    # Default is 'None'. Set to eg 'page_size' to enable usage.
    # page_size_query_param 和 page_size_query_description:定義了查詢引數名稱和描述,用於表示每頁大小。
    page_size_query_param = None
    page_size_query_description = _('Number of results to return per page.')

    # Set to an integer to limit the maximum page size the client may request.
    # Only relevant if 'page_size_query_param' has also been set.
    
    # max_page_size:定義了客戶端可以請求的最大頁面大小
    max_page_size = None

    # The offset in the cursor is used in situations where we have a
    # nearly-unique index. (Eg millisecond precision creation timestamps)
    # We guard against malicious users attempting to cause expensive database
    # queries, by having a hard cap on the maximum possible size of the offset.
    
    # offset_cutoff:定義了遊標的最大偏移量,以防止惡意使用者發出昂貴的資料庫查詢
    offset_cutoff = 1000

    def paginate_queryset(self, queryset, request, view=None):
        # 獲取請求中的頁大小(self.page_size),
        self.page_size = self.get_page_size(request)
        # 如果沒有指定頁大小則返回None,表示不進行分頁
        if not self.page_size:
            return None
		
        # 獲取請求的基礎URL(self.base_url)以及排序方式(self.ordering)
        self.base_url = request.build_absolute_uri()
        self.ordering = self.get_ordering(request, queryset, view)
		
        # 解碼遊標(self.cursor):如果請求中包含遊標引數,則解碼遊標值,否則建立一個初始遊標。
        self.cursor = self.decode_cursor(request)
        

        # 檢查遊標是否存在。
        # 如果遊標不存在(即self.cursor為None),則建立一個初始遊標
        # 其中offset為0,reverse為False,current_position為None。這是遊標不存在時的預設設定。
        if self.cursor is None:
            (offset, reverse, current_position) = (0, False, None)
        else:
            (offset, reverse, current_position) = self.cursor

        # Cursor pagination always enforces an ordering.

        # 根據遊標分頁查詢:根據請求中的排序方式和遊標資訊,對查詢集(queryset)進行排序,並根據遊標資訊進行過濾。
        # 遊標分頁始終需要按照某種排序方式進行分頁,以確保分頁結果的一致性。
        if reverse:
            # 根據遊標分頁的要求強制進行排序。如果reverse為True,則對查詢集進行反向排序,以確保按照正確的順序分頁。
            queryset = queryset.order_by(*_reverse_ordering(self.ordering))
        else:
            # 否則,按照正常的排序方式排序
            queryset = queryset.order_by(*self.ordering)

        # If we have a cursor with a fixed position then filter by that.
        
        # 獲取分頁結果:根據遊標資訊和頁大小,從查詢結果中獲取一頁的資料,同時獲取一頁後面的一個額外項。
        # 這個額外項用於確定是否有下一頁。
        if current_position is not None:
            # 如果遊標具有固定位置(即current_position不為None),則根據遊標資訊新增過濾條件。這是為了確保分頁結果正確。
            order = self.ordering[0]
            is_reversed = order.startswith('-')
            order_attr = order.lstrip('-')

            # Test for: (cursor reversed) XOR (queryset reversed)
            # 具體來說,它檢查遊標的排序方式和查詢集的排序方式是否一致
            if self.cursor.reverse != is_reversed:
                # 如果不一致,則使用不同的過濾條件
                kwargs = {order_attr + '__lt': current_position}
            else:
                kwargs = {order_attr + '__gt': current_position}
			
            # 接下來,它執行實際的查詢,從查詢結果中獲取一頁的資料。
            queryset = queryset.filter(**kwargs)

        # If we have an offset cursor then offset the entire page by that amount.
        # We also always fetch an extra item in order to determine if there is a
        # page following on from this one.
        	
        # 為了確定是否有下一頁,它額外獲取一頁的資料。這是為了避免在瀏覽下一頁時再次向資料庫發出查詢請求,從而提高效能。
        results = list(queryset[offset:offset + self.page_size + 1])
        self.page = list(results[:self.page_size])

        # Determine the position of the final item following the page.
        # 最後,它確定是否有下一頁(has_following_position為True表示有下一頁),以及下一頁的位置(following_position)。
        # 這將用於構建下一頁的遊標。
        if len(results) > len(self.page):
            has_following_position = True
            following_position = self._get_position_from_instance(results[-1], self.ordering)
        else:
            has_following_position = False
            following_position = None
		
        # 如果reverse為True,這表示查詢集是反向排序的,因此在返回給使用者之前
        if reverse:
            # If we have a reverse queryset, then the query ordering was in reverse
            # so we need to reverse the items again before returning them to the user.
            
            # 需要將self.page中的結果反轉,以確保它們按照正確的順序呈現。
            self.page = list(reversed(self.page))

            # Determine next and previous positions for reverse cursors.
            
            # 根據遊標資訊和當前位置(current_position)以及偏移量(offset)來確定是否有前一頁(has_previous)和後一頁(has_next)。
            self.has_next = (current_position is not None) or (offset > 0)
            
            # 如果存在前一頁或後一頁,還會設定相應的遊標位置,以便構建前一頁和後一頁的遊標連結。
            self.has_previous = has_following_position
            if self.has_next:
                # next_position表示下一頁的位置
                self.next_position = current_position
            if self.has_previous:
                # previous_position表示前一頁的位置。
                self.previous_position = following_position
        else:
            # Determine next and previous positions for forward cursors.
            
            self.has_next = has_following_position
            self.has_previous = (current_position is not None) or (offset > 0)
            if self.has_next:
                self.next_position = following_position
            if self.has_previous:
                self.previous_position = current_position

        # Display page controls in the browsable API if there is more
        # than one page.
        # 如果存在前一頁或後一頁,並且模板(template)已設定
        if (self.has_previous or self.has_next) and self.template is not None:
            # 將display_page_controls設定為True,這表示在可瀏覽的API中會顯示分頁控制元件,以便使用者導航到前一頁或後一頁
            self.display_page_controls = True
            

        return self.page
	
    # 從HTTP請求中獲取每頁資料條目數量(分頁大小)
    def get_page_size(self, request):
        # 檢查是否定義了self.page_size_query_param屬性。
        # 這個屬性通常用於指定客戶端可以在請求中使用的查詢引數,以控制每頁資料的數量。
        # 如果self.page_size_query_param不為None,則表示你允許客戶端透過查詢引數來自定義每頁資料的數量。
        if self.page_size_query_param:
            try:
                # 如果允許客戶端自定義每頁資料的數量,它嘗試從HTTP請求的查詢引數(request.query_params)中獲取指定的查詢引數的值,該查詢引數通常是一個整數,用於指定每頁資料的數量。
                # 如果成功獲取到查詢引數的值,並且該值是一個正整數(透過_positive_int函式進行檢查),則返回這個正整數作為每頁資料的數量。
                # 同時,它還使用strict=True引數來確保只接受正整數,並使用cutoff=self.max_page_size引數來限制每頁資料數量不超過self.max_page_size,以防止客戶端請求非常大的分頁。
                return _positive_int(
                    request.query_params[self.page_size_query_param],
                    strict=True,
                    cutoff=self.max_page_size
                )
           
        	# 如果無法獲取查詢引數的值、查詢引數的值不是正整數、或者超過了最大允許的每頁資料數量(如果有限制),則會捕獲KeyError(查詢引數不存在)和ValueError(值不是正整數)異常,並繼續執行下一步。
            except (KeyError, ValueError):
                pass
	
    	# 如果無法獲取有效的查詢引數值,或者未定義self.page_size_query_param,則返回預設的每頁資料數量,即self.page_size。
        return self.page_size
	
    # 用於生成下一頁的連結的方法,該方法會根據當前的分頁狀態和遊標資訊生成下一頁的連結。
    def get_next_link(self):
        # 檢查 self.has_next,這個屬性表示是否存在下一頁。
        # 如果不存在下一頁(self.has_next 為 False),則返回 None,表示沒有下一頁連結可生成。
        if not self.has_next:
            return None
		
        # 檢查分頁方向和遊標資訊,以決定如何生成下一頁的連結。
        # 遊標分頁可以有兩個方向:正向和反向(根據排序方向)。
        # 正向表示按照升序排序,反向表示按照降序排序。
        if self.page and self.cursor and self.cursor.reverse and self.cursor.offset != 0:
            # If we're reversing direction and we have an offset cursor
            # then we cannot use the first position we find as a marker.
            # 如果當前是反向分頁(self.cursor.reverse 為 True)並且遊標的偏移量不為零(self.cursor.offset != 0),則表示當前頁資料已經反向排序,且存在遊標偏移,因此不能使用第一個位置作為標記位置(marker position)。
            compare = self._get_position_from_instance(self.page[-1], self.ordering)
        else:
            # 否則,使用 self.next_position 作為比較位置(compare position),它表示下一頁資料的起始位置。
            compare = self.next_position
            
        # 同時,初始化 offset 為 0,用於跟蹤需要跳過的資料項數量。
        offset = 0
	
    	# 遍歷當前頁的資料項,從最後一個資料項開始向前遍歷,
        has_item_with_unique_position = False
        for item in reversed(self.page):
            # 獲取每個資料項的位置資訊(透過 _get_position_from_instance 方法),並與 compare 進行比較。
            position = self._get_position_from_instance(item, self.ordering)
            # 如果某個資料項的位置與 compare 不相等,說明該位置可以作為標記位置,表示下一頁的資料開始。
            if position != compare:
                # The item in this position and the item following it
                # have different positions. We can use this position as
                # our marker.
                # 於是,將 has_item_with_unique_position 設定為 True,並退出遍歷
                has_item_with_unique_position = True
                break

            # The item in this position has the same position as the item
            # following it, we can't use it as a marker position, so increment
            # the offset and keep seeking to the previous item.
            
            compare = position
            offset += 1
		
        # 如果遍歷完整個當前頁,但沒有找到唯一位置,表示當前頁的資料項位置都相同,此時需要根據不同情況來確定下一頁的遊標資訊
        if self.page and not has_item_with_unique_position:
            # There were no unique positions in the page.
            # 如果當前是第一頁且沒有上一頁,表示已經處於第一頁且沒有更多的資料了
            # 此時將 offset 設定為 self.page_size(下一頁的遊標偏移量)
            # 並將 position 設定為 None。
            if not self.has_previous:
                # We are on the first page.
                # Our cursor will have an offset equal to the page size,
                # but no position to filter against yet.
                offset = self.page_size
                position = None
                
            # 如果當前是反向分頁,說明當前頁是最後一頁,但由於反向分頁的特性,可能會有額外的資料項需要跳過
            # 此時將 offset 設定為 0,表示下一頁的遊標從資料的開始位置開始
            # 同時將 position 設定為 self.previous_position,表示下一頁的遊標位置。
            elif self.cursor.reverse:
                # The change in direction will introduce a paging artifact,
                # where we end up skipping forward a few extra items.
                offset = 0
                position = self.previous_position
                
            # 如果不是以上兩種情況,表示在正向分頁中,使用遊標資訊來確定下一頁的遊標。
            # 將 offset 設定為 self.cursor.offset + self.page_size,表示下一頁的遊標偏移量為當前遊標偏移量加上一頁資料的大小,
            # 同時將 position 設定為 self.previous_position,表示下一頁的遊標位置。
            else:
                # Use the position from the existing cursor and increment
                # it's offset by the page size.
                offset = self.cursor.offset + self.page_size
                position = self.previous_position
		
        # 如果當前頁沒有資料(not self.page),則將 position 設定為 self.next_position,表示下一頁的遊標位置
        if not self.page:
            position = self.next_position
		
        # 最後,根據生成的 offset、position 和分頁方向(正向)建立一個新的遊標物件(Cursor
        # 然後呼叫 encode_cursor 方法將遊標物件編碼為遊標字串,並返回生成的下一頁連結。
        cursor = Cursor(offset=offset, reverse=False, position=position)
        return self.encode_cursor(cursor)
	
    # 用於生成上一頁的連結。上一頁的連結通常包含在分頁 API 響應中,以便客戶端可以方便地請求上一頁的資料
    def get_previous_link(self):
        
        # 檢查 self.has_previous,這個屬性表示是否存在上一頁。
        # 如果不存在上一頁(self.has_previous 為 False),則返回 None,表示沒有上一頁連結可生成。
        if not self.has_previous:
            return None
		
        # 檢查分頁方向和遊標資訊,以決定如何生成上一頁的連結。
        # 遊標分頁可以有兩個方向:正向和反向(根據排序方向)。正向表示按照升序排序,反向表示按照降序排序。
        if self.page and self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
            # If we're reversing direction and we have an offset cursor
            # then we cannot use the first position we find as a marker.
            # 如果當前是正向分頁(not self.cursor.reverse 為 True)並且遊標的偏移量不為零(self.cursor.offset != 0),則表示當前頁資料已經正向排序,且存在遊標偏移
            # 因此不能使用第一個位置作為標記位置(marker position)。
            compare = self._get_position_from_instance(self.page[0], self.ordering)
        else:
            # 否則,使用 self.previous_position 作為比較位置(compare position),它表示上一頁資料的起始位置。
            compare = self.previous_position
        # 同時,初始化 offset 為 0,用於跟蹤需要跳過的資料項數量。
        offset = 0
		
        # 
        has_item_with_unique_position = False
        # 遍歷當前頁的資料項,從第一個資料項開始向後遍歷
        for item in self.page:
            # 獲取每個資料項的位置資訊(透過 _get_position_from_instance 方法),並與 compare 進行比較。
            position = self._get_position_from_instance(item, self.ordering)
            # 如果某個資料項的位置與 compare 不相等,說明該位置可以作為標記位置,表示上一頁的資料開始。
            if position != compare:
                # The item in this position and the item following it
                # have different positions. We can use this position as
                # our marker.
                # 於是,將 has_item_with_unique_position 設定為 True,並退出遍歷。
                has_item_with_unique_position = True
                break

            # The item in this position has the same position as the item
            # following it, we can't use it as a marker position, so increment
            # the offset and keep seeking to the previous item.
            
            
            compare = position
            offset += 1
		
        # 如果遍歷完整個當前頁,但沒有找到唯一位置,表示當前頁的資料項位置都相同,此時需要根據不同情況來確定上一頁的遊標資訊
        if self.page and not has_item_with_unique_position:
            # There were no unique positions in the page.
            
            # 如果當前是最後一頁且沒有下一頁,表示已經處於最後一頁且沒有更多的資料了,此時將 offset 設定為 self.page_size(上一頁的遊標偏移量),並將 position 設定為 None。
            if not self.has_next:
                # We are on the final page.
                # Our cursor will have an offset equal to the page size,
                # but no position to filter against yet.
                offset = self.page_size
                position = None
                
            # 如果當前是反向分頁,說明當前頁是第一頁,但由於反向分頁的特性,可能會有額外的資料項需要跳過,此時將 offset 設定為 0,表示上一頁的遊標從資料的開始位置開始,同時將 position 設定為 self.next_position,表示上一頁的遊標位置。
            elif self.cursor.reverse:
                # Use the position from the existing cursor and increment
                # it's offset by the page size.
                offset = self.cursor.offset + self.page_size
                position = self.next_position
                
            # 如果不是以上兩種情況,表示在正向分頁中,使用遊標資訊來確定上一頁的遊標。
            # 將 offset 設定為 self.cursor.offset + self.page_size,表示上一頁的遊標偏移量為當前遊標偏移量加上一頁資料的大小,同時將 position 設定為 self.next_position,表示上一頁的遊標位置。
            else:
                # The change in direction will introduce a paging artifact,
                # where we end up skipping back a few extra items.
                offset = 0
                position = self.next_position
		
        # 如果當前頁沒有資料(not self.page),則將 position 設定為 self.previous_position,表示上一頁的遊標位置。
        if not self.page:
            position = self.previous_position
		
        # 最後,根據生成的 offset、position 和分頁方向(反向)建立一個新的遊標物件(Cursor)
        # 然後呼叫 encode_cursor 方法將遊標物件編碼為遊標字串,並返回生成的上一頁連結。
        cursor = Cursor(offset=offset, reverse=True, position=position)
        return self.encode_cursor(cursor)
	
    # self(Pagination 例項),request(Django 請求物件),queryset(查詢集),和 view(Django Rest Framework 檢視例項)。
    # 該方法的目標是返回一個可以用於 Django 查詢集的排序方式,通常是一個包含欄位名的元組。
    def get_ordering(self, request, queryset, view):
        """
        Return a tuple of strings, that may be used in an `order_by` method.
        """
        
        # 定義了一個名為 ordering_filters 的列表,用於儲存檢視中已定義了 get_ordering 方法的過濾器類。
        # 這些過濾器類通常用於處理檢視中的排序邏輯。
        # 過濾器類是根據檢視的 filter_backends 屬性確定的,如果過濾器類實現了 get_ordering 方法,它就會被包含在 ordering_filters 列表中。
        ordering_filters = [
            filter_cls for filter_cls in getattr(view, 'filter_backends', [])
            if hasattr(filter_cls, 'get_ordering')
        ]
		
        # 檢查 ordering_filters 是否存在。
        
        
        if ordering_filters:
            # If a filter exists on the view that implements `get_ordering`
            # then we defer to that filter to determine the ordering.
            
            # 如果存在過濾器類實現了 get_ordering 方法
            # 程式碼會選擇第一個過濾器類(ordering_filters[0])並建立其例項(filter_instance)
            filter_cls = ordering_filters[0]
            filter_instance = filter_cls()
            # 然後呼叫過濾器的 get_ordering 方法來獲取排序方式。
            ordering = filter_instance.get_ordering(request, queryset, view)
            assert ordering is not None, (
                'Using cursor pagination, but filter class {filter_cls} '
                'returned a `None` ordering.'.format(
                    filter_cls=filter_cls.__name__
                )
            )
        else:
            # The default case is to check for an `ordering` attribute
            # on this pagination instance.
            
            # 如果存在過濾器類併成功獲取排序方式,則返回這個排序方式。
            # 否則,程式碼會繼續執行預設的排序方式。
            # 預設的排序方式是從 self.ordering 中獲取的,其中 self 是分頁例項的屬性。
            # 如果分頁類沒有定義 ordering 屬性,則會觸發一個斷言錯誤,提示需要在分頁類上宣告排序方式。
            # 這個排序方式通常是一個字串,表示要按哪個欄位排序,或者是一個包含多個欄位的元組。
            ordering = self.ordering
            assert ordering is not None, (
                'Using cursor pagination, but no ordering attribute was declared '
                'on the pagination class.'
            )
            assert '__' not in ordering, (
                'Cursor pagination does not support double underscore lookups '
                'for orderings. Orderings should be an unchanging, unique or '
                'nearly-unique field on the model, such as "-created" or "pk".'
            )

        assert isinstance(ordering, (str, list, tuple)), (
            'Invalid ordering. Expected string or tuple, but got {type}'.format(
                type=type(ordering).__name__
            )
        )
		
        # 程式碼檢查排序方式的型別,如果是字串,則將其封裝成一個元組返回,以符合 Django 查詢集排序的要求。
        # 如果排序方式已經是元組或列表形式,直接返回
        if isinstance(ordering, str):
            return (ordering,)
        return tuple(ordering)
	
    # 解碼請求中的遊標,並返回一個 Cursor 例項。遊標通常用於標識在查詢結果集中的當前位置。
    def decode_cursor(self, request):
        """
        Given a request with a cursor, return a `Cursor` instance.
        """
        # Determine if we have a cursor, and if so then decode it.
        
        # 嘗試從請求的查詢引數中獲取遊標資訊,查詢引數的名稱由 self.cursor_query_param 指定。
        # 如果沒有找到對應的查詢引數,說明客戶端沒有提供遊標,此時返回 None。
        encoded = request.query_params.get(self.cursor_query_param)
        if encoded is None:
            return None

        try:
            # 如果成功獲取到遊標的編碼字串,程式碼進一步解碼它。
            # 遊標通常以某種編碼方式進行傳輸,這裡使用了 Base64 編碼。
            # 首先,程式碼透過 b64decode 函式將編碼字串解碼成二進位制資料,然後將其再次解碼成 ASCII 字串。
            querystring = b64decode(encoded.encode('ascii')).decode('ascii')
            			
            # 解碼後的遊標字串通常包含多個部分,如 o(偏移量)、r(是否反向排序)和 p(位置)。
            # 程式碼使用 Python 的 parse_qs 函式解析這些部分,將它們提取為字典 tokens。
            # keep_blank_values=True 參數列示即使某些部分沒有值也要保留鍵。
            tokens = parse.parse_qs(querystring, keep_blank_values=True)
		
            # 從 tokens 字典中提取遊標的各個部分,包括 offset(偏移量)、reverse(是否反向排序)和 position(位置)。
            # 這些部分通常以字串形式儲存。
            offset = tokens.get('o', ['0'])[0]
            # offset 部分表示當前遊標的偏移量,通常用於確定查詢結果集的起始位置。
            # 程式碼使用 _positive_int 函式將 offset 轉換為正整數,並根據 cutoff 屬性來進行截斷,以限制最大偏移量的大小。
            offset = _positive_int(offset, cutoff=self.offset_cutoff)
			
            # reverse 部分表示是否應該反向排序查詢結果集。
            # 它通常是一個布林值,程式碼將其轉換為布林型別。
            reverse = tokens.get('r', ['0'])[0]
            reverse = bool(int(reverse))
			
            # position 部分通常表示遊標的當前位置,用於標識在查詢結果集中的具體位置。
            # 位置可以是一個字串或 None,程式碼直接提取並儲存。
            position = tokens.get('p', [None])[0]
        except (TypeError, ValueError):
            # 如果在解碼遊標的過程中發生了任何異常(如型別錯誤或數值錯誤),程式碼會丟擲 NotFound 異常,這表示遊標無效。
            raise NotFound(self.invalid_cursor_message)
		
        # 最後,程式碼使用提取的 offset、reverse 和 position 建立一個 Cursor 例項,並將其返回。
        # Cursor 是一個自定義的資料結構,用於表示遊標資訊。
        return Cursor(offset=offset, reverse=reverse, position=position)
	
    # 接受一個 Cursor 例項作為引數,該例項包含遊標的資訊,包括偏移量、反向排序標誌和位置
    def encode_cursor(self, cursor):
        """
        Given a Cursor instance, return an url with encoded cursor.
        """
        # 建立一個空字典 tokens,用於儲存遊標的各個部分
        tokens = {}
        
        # 如果遊標的偏移量不為零(即 cursor.offset != 0)
        # 則將偏移量部分新增到 tokens 字典中,使用鍵 'o'(表示偏移量)和偏移量的字串表示形式。
        if cursor.offset != 0:
            tokens['o'] = str(cursor.offset)
            
        # 如果遊標的反向排序標誌為 True(即 cursor.reverse 為 True)
        # 則將反向排序部分新增到 tokens 字典中,使用鍵 'r' 和值 '1' 表示。
        if cursor.reverse:
            tokens['r'] = '1'
            
        # 如果遊標的位置不為 None(即 cursor.position is not None)
        # 則將位置部分新增到 tokens 字典中,使用鍵 'p' 和位置的字串表示形式。
        if cursor.position is not None:
            tokens['p'] = cursor.position
		
        # 使用 parse.urlencode 函式將 tokens 字典編碼為查詢字串形式的鍵值對。
        # doseq=True 引數確保多個值具有相同的鍵時,生成多個鍵值對。
        querystring = parse.urlencode(tokens, doseq=True)
        
        # 使用 b64encode 函式將查詢字串編碼為 ASCII 字串的 Base64 編碼形式。這是為了將遊標資訊轉換為可安全傳輸的字串。
        encoded = b64encode(querystring.encode('ascii')).decode('ascii')
        
        # 使用 replace_query_param 函式將編碼後的遊標字串新增到請求的 URL 中,以替換原始 URL 中的遊標引數。
        # 這確保了響應中的 URL 包含了新的遊標資訊
        return replace_query_param(self.base_url, self.cursor_query_param, encoded)
	
    # 從資料物件(通常是查詢結果的一項)中提取位置資訊
    # instance:要從中提取位置資訊的資料物件。
    # ordering:表示資料排序方式的元組或字串。
    def _get_position_from_instance(self, instance, ordering):
        
        # 首先從排序方式 ordering 中提取排序欄位的名稱(去除可能的負號)
        field_name = ordering[0].lstrip('-')
        if isinstance(instance, dict):
            # 然後根據資料物件 instance 的型別(是否為字典)來提取相應的屬性或字典鍵的值。
            attr = instance[field_name]
        else:
            attr = getattr(instance, field_name)
            
        # 最後,將提取的值轉換為字串,並將其作為位置資訊返回。
        return str(attr)

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))
	
    # 構建包含分頁資訊的響應
    # 接受一個引數 data,它是要包含在響應中的分頁資料
    def get_paginated_response_schema(self, schema):
        # 首先構建一個有序字典(OrderedDict),其中包括以下鍵值對
        return {
            # 
            'type': 'object',
            'properties': {
                # 'next':透過呼叫 self.get_next_link() 方法獲取下一頁的連結。
                'next': {
                    'type': 'string',
                    'nullable': True,
                },
                # 'previous':透過呼叫 self.get_previous_link() 方法獲取上一頁的連結。
                'previous': {
                    'type': 'string',
                    'nullable': True,
                },
                # 'results':包含實際分頁資料的鍵,即引數 data。
                'results': schema,
            },
        }
	
    # 返回一個字典,其中包含上一頁和下一頁的連結 URL。
    # 這個方法主要用於構建分頁控制元件的 HTML 上下文資料
    def get_html_context(self):
        return {
            # 'previous_url':透過呼叫 self.get_previous_link() 方法獲取上一頁的連結 URL。
            'previous_url': self.get_previous_link(),
            # 'next_url':透過呼叫 self.get_next_link() 方法獲取下一頁的連結 URL。
            'next_url': self.get_next_link()
        }
	
    # 生成 HTML 渲染的分頁控制元件內容
    def to_html(self):
        # 首先獲取分頁模板(self.template)並使用 Django 模板載入器 (loader) 獲取模板物件。
        # 然後,它獲取上述的 HTML 上下文資料(透過呼叫 self.get_html_context() 方法),將這些資料傳遞給模板
        # 最後返回渲染後的 HTML 內容。
        template = loader.get_template(self.template)
        context = self.get_html_context()
        
        # 返回的 HTML 內容通常包括上一頁和下一頁的連結,以及其他分頁控制元件(如頁碼導航等),允許使用者在瀏覽器中進行分頁導航
        return template.render(context)
	
    # 獲取分頁器(Paginator)的 schema 欄位列表,以便在文件生成和 API 除錯時使用
    def get_schema_fields(self, view):
        
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
        # 遊標欄位(Cursor Field)
        fields = [
            coreapi.Field(
                # name: 欄位的名稱,通常是 self.cursor_query_param 的值,表示遊標引數的名稱。
                name=self.cursor_query_param,
                # required: 指示是否必須提供此引數。在這裡,遊標引數是可選的,因此設定為 False。
                required=False,
                # location: 引數的位置,通常是 'query',表示引數位於查詢字串中。
                location='query',
                # schema: 欄位的 schema 描述,用於指定欄位的型別和描述資訊
                schema=coreschema.String(
                    # title: 欄位的標題,通常是 'Cursor',表示遊標欄位的標題
                    title='Cursor',
                    # description: 欄位的描述資訊,通常是 'Cursor pagination cursor value',表示遊標欄位的描述。
                    description=force_str(self.cursor_query_description)
                )
            )
        ]
        # 
        if self.page_size_query_param is not None:
            # 頁大小欄位(Page Size Field)
            fields.append(
                coreapi.Field(
                    # name: 欄位的名稱,通常是 self.page_size_query_param 的值,表示頁大小引數的名稱。
                    name=self.page_size_query_param,
                    # required: 指示是否必須提供此引數。在這裡,頁大小引數是可選的,因此設定為 False。
                    required=False,
                    # location: 引數的位置,通常是 'query',表示引數位於查詢字串中。
                    location='query',
                    # schema: 欄位的 schema 描述,用於指定欄位的型別和描述資訊
                    schema=coreschema.Integer(
                        # title: 欄位的標題,通常是 'Page size',表示頁大小欄位的標題
                        title='Page size',
                        # description: 欄位的描述資訊,通常是 'Number of results to return per page',表示頁大小欄位的描述。
                        description=force_str(self.page_size_query_description)
                    )
                )
            )
        return fields

    def get_schema_operation_parameters(self, view):
        # 遊標引數(Cursor Parameter)
        parameters = [
            {
                # name: 引數的名稱,通常是 self.cursor_query_param 的值,表示遊標引數的名稱。
                'name': self.cursor_query_param,
                # required: 指示是否必須提供此引數。在這裡,遊標引數是可選的,因此設定為 False。
                'required': False,
                # in: 引數的位置,通常是 'query',表示引數位於查詢字串中。
                'in': 'query',
                # description: 引數的描述資訊,通常是 'Cursor pagination cursor value',表示遊標引數的描述。
                'description': force_str(self.cursor_query_description),
                # schema: 引數的 schema 描述,用於指定引數的型別。在這裡,遊標引數的型別被設定為 'string',表示它是一個字串。
                'schema': {
                    'type': 'string',
                },
            }
        ]
        # 頁大小引數(Page Size Parameter)
        if self.page_size_query_param is not None:
            parameters.append(
                {
                    # name: 引數的名稱,通常是 self.page_size_query_param 的值,表示頁大小引數的名稱。
                    'name': self.page_size_query_param,
                    # required: 指示是否必須提供此引數。在這裡,頁大小引數是可選的,因此設定為 False。
                    'required': False,
                    # in: 引數的位置,通常是 'query',表示引數位於查詢字串中。
                    'in': 'query',
                    # description: 引數的描述資訊,通常是 'Number of results to return per page',表示頁大小引數的描述。
                    'description': force_str(self.page_size_query_description),
                    # schema: 引數的 schema 描述,用於指定引數的型別。在這裡,頁大小引數的型別被設定為 'integer',表示它是一個整數。
                    'schema': {
                        'type': 'integer',
                    },
                }
            )
        return parameters

【八】自定義分頁

【1】自定義分頁 ---- 返回全部資料

class BookView(APIView):
    back_dict = {"code": 1000, "msg": "", "result": []}

    def get(self, request):

        # 排序條件
        order_param = request.query_params.get('ordering')
        # 過濾條件
        filter_name = request.query_params.get('name')
        book_obj = Book.objects.all()
        if order_param:
            book_obj = book_obj.order_by(order_param)
        if filter_name:
            # 包含過濾條件的被過濾出來
            book_obj = book_obj.filter(name__contains=filter_name)

        # 分頁
        pagination = BookLimitOffsetPagination()
        page = pagination.paginate_queryset(book_obj, request, self)

        # 序列化
        book_ser = BookSerializer(instance=page, many=True)

        self.back_dict["msg"] = "請求資料成功"
        # self.back_dict["result"] = pagination.get_paginated_response(book_ser.data)

        return pagination.get_paginated_response(book_ser.data)
{
    "count": 6,
    "next": "http://127.0.0.1:8000/app01/v1/books/?limit=2&offset=2",
    "previous": null,
    "results": [
        {
            "id": 1,
            "name": "a",
            "price": 44
        },
        {
            "id": 2,
            "name": "b",
            "price": 666
        }
    ]
}
  • 上述程式碼實現了一個自定義分頁功能,透過 BookLimitOffsetPagination 類對 book_obj 進行分頁處理,並返回分頁結果。
  • get 方法中,首先獲取請求引數中的排序條件 order_param 和過濾條件 filter_name。然後透過 Book.objects.all() 獲取所有的 Book 物件。
  • 接下來,根據排序條件和過濾條件對 book_obj 進行排序和過濾操作,得到符合條件的查詢結果。
  • 然後,建立 BookLimitOffsetPagination 類的例項 pagination。透過呼叫 pagination.paginate_queryset(book_obj, request, self) 方法對查詢結果進行分頁處理,其中 request 是當前請求物件,self 是當前檢視物件。
  • 接著,使用序列化器 BookSerializer 對分頁後的結果 page 進行序列化,得到序列化後的資料 book_ser
  • 之後,更新 self.back_dict"msg" 鍵的值為 "請求資料成功"。
  • 最後,透過呼叫 pagination.get_paginated_response(book_ser.data) 方法,將序列化後的分頁資料傳入,該方法會返回包含分頁資訊的字典物件作為響應結果。
  • 綜上所述,當訪問 {{host}}app01/v1/books/ 時,會返回一個帶有分頁資訊的響應結果,其中 "count" 表示總數,"next" 表示下一頁連結,"previous" 表示上一頁連結,"results" 表示當前頁資料。

【2】自定義分頁 ---- 返回自定義資料格式

class BookView(APIView):
    back_dict = {"code": 1000, "msg": "", "result": []}

    def get(self, request):

        # 排序條件
        order_param = request.query_params.get('ordering')
        # 過濾條件
        filter_name = request.query_params.get('name')
        book_obj = Book.objects.all()
        if order_param:
            book_obj = book_obj.order_by(order_param)
        if filter_name:
            # 包含過濾條件的被過濾出來
            book_obj = book_obj.filter(name__contains=filter_name)

        # 分頁
        pagination = BookLimitOffsetPagination()
        page = pagination.paginate_queryset(book_obj, request, self)

        # 序列化
        book_ser = BookSerializer(instance=page, many=True)

        self.back_dict["msg"] = "請求資料成功"
        '''
        # get_paginated_response - - 可以指定返回的資料
		def get_paginated_response(self, data):
    		return Response(OrderedDict([
                ('count', self.count),
                ('next', self.get_next_link()),
                ('previous', self.get_previous_link()),
                ('results', data)
   			 ]))
        '''
        self.back_dict['count'] = pagination.count
        self.back_dict['next'] = pagination.get_next_link()

        return Response(self.back_dict)
{
    "code": 1000,
    "msg": "請求資料成功",
    "result": [],
    "count": 6,
    "next": "http://127.0.0.1:8000/app01/v1/books/?limit=2&offset=2"
}

相關文章