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
:這是最常見的分頁類,它使用頁碼來分割資料。
【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
的分頁類
- 類屬性說明:
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
:這種分頁類使用限制和偏移量來分頁,允許你指定返回的結果數量和從哪裡開始。
【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
:這是一種基於遊標的分頁,適用於需要深度分頁的情況,如社交媒體應用。
【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"
}