DRF之分页类源码分析

发布时间 2023-09-19 17:04:12作者: Chimengmeng

【一】分页类介绍

  • 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"
}