DRF之分页类源码分析
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" }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通