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"
}
标签:分页,get,self,param,源码,offset,query,page,DRF
From: https://www.cnblogs.com/ssrheart/p/18153597