首页 > 编程语言 >DRF之分页类源码分析

DRF之分页类源码分析

时间:2023-09-19 17:15:15浏览次数:44  
标签:分页 get self param 源码 offset query page DRF

【一】分页类介绍

  • Django REST framework(DRF)是一个用于构建Web API的强大工具,它提供了分页功能,使你能够控制API响应的数据量。
  • 在DRF中,分页功能由分页类(Paginator Class)来管理。

【二】内置分页类

  • 在DRF中,分页类通常位于rest_framework.pagination模块中,它们用于分割长列表或查询集,以便在API响应中只返回一部分数据。以下是一些常见的DRF分页类:

    • PageNumberPagination:这是最常见的分页类,它使用页码来分割数据。

    • LimitOffsetPagination:这种分页类使用限制和偏移量来分页,允许你指定返回的结果数量和从哪里开始。

    • CursorPagination:这是一种基于游标的分页,适用于需要深度分页的情况,如社交媒体应用。

    • CustomPagination:你还可以自定义自己的分页类,以满足特定需求。

【三】分页类的执行流程

  • 请求到达DRF视图:
    • 当一个API请求到达DRF视图时,DRF视图会根据视图的配置和查询参数来选择使用哪个分页类。
    • 通常,你可以在视图类中设置pagination_class属性来指定使用的分页类。
  • 实例化分页类:
    • 一旦确定了要使用的分页类,DRF将实例化该分页类的对象。
    • 这个对象将在后续的处理中负责执行分页操作。
  • 查询数据:
    • 视图从数据库或其他数据源查询数据,并将数据传递给分页类的实例。
  • 分页数据:
    • 分页类根据查询参数(如页码、每页数量等)对数据进行分页,并返回一个包含分页结果的序列化对象。
  • 构建API响应:
    • 视图将包含分页结果的序列化对象添加到API响应中,并返回给客户端

【四】基础分页

class BasePagination:
    display_page_controls = False

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

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

    def get_paginated_response_schema(self, schema):
        return schema

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

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

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

    def get_schema_operation_parameters(self, view):
        return []

【五】基本分页PageNumberPagination

  • PageNumberPagination:这是最常见的分页类,它使用页码来分割数据。

【1】使用

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


class BookNumberPagination(PageNumberPagination):
    # 重写 4 个 类属性
    page_size = 2  # 每页显示的条数

    page_query_param = 'page'  # 路径后面的 参数:page=4(第4页)

    page_size_query_param = 'page_size'  # page=4&page_size=5:查询第4页,每页显示5条

    max_page_size = 5  # 每页最多显示5条
  • 自定义了一个名为BookNumberPagination的分页类
    • 继承自PageNumberPagination
  • 类属性说明:
    • page_size:每页显示的条数,默认值为2条。
    • page_query_param:路径后面指定的参数名,默认为page
      • 例如,http://127.0.0.1:8000/app01/v1/books/?page=4表示查询第4页的数据。
    • page_size_query_param:路径后面指定的参数名,表示每页显示的条数,默认为page_size
      • 例如,http://127.0.0.1:8000/app01/v1/books/?page=4&page_size=5表示查询第4页的数据,每页显示5条。
    • max_page_size:每页最多显示的条数,默认值为5条。
  • 返回结果说明:
    • count:符合查询条件的总记录数,即所有记录的数量。
    • next:下一页的URL链接,如果有下一页数据,则返回对应的URL;否则返回null。
    • previous:上一页的URL链接,如果有上一页数据,则返回对应的URL;否则返回null。
    • results:当前页的数据列表。

【2】源码分析

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

    http://api.example.org/accounts/?page=4
    http://api.example.org/accounts/?page=4&page_size=100
    """
    # The default page size.
    # Defaults to `None`, meaning pagination is disabled.
    
    # 默认每页的数量。如果没有设置page_size_query_param,则默认为此值。
    page_size = api_settings.PAGE_SIZE
	
    # 用于分页的Django分页器类。默认是DjangoPaginator,它用于根据page_size将查询集分页。
    django_paginator_class = DjangoPaginator

    # Client can control the page using this query parameter.
    
    # 客户端可以使用的查询参数来控制页码。默认是'page'
    page_query_param = 'page'
    
    # 页码查询参数的描述
    page_query_description = _('A page number within the paginated result set.')

    # Client can control the page size using this query parameter.
    # Default is 'None'. Set to eg 'page_size' to enable usage.
    
    # 客户端可以使用的查询参数来控制每页的数量。默认是None,表示不启用此功能。
    page_size_query_param = None
    
    # 每页数量查询参数的描述
    page_size_query_description = _('Number of results to return per page.')

    # Set to an integer to limit the maximum page size the client may request.
    # Only relevant if 'page_size_query_param' has also been set.
    
    # 用于限制客户端可请求的最大每页数量的整数。仅在page_size_query_param已启用时有效。
    max_page_size = None
	
    # 字符串列表,表示最后一页的字符串描述。默认为('last',)。
    last_page_strings = ('last',)
	
    # 分页HTML模板的路径,默认为'rest_framework/pagination/numbers.html'
    template = 'rest_framework/pagination/numbers.html'
	
    # 无效页码时的错误消息
    invalid_page_message = _('Invalid page.')
	
    # 在视图中执行分页操作。
    # queryset(查询集),request(请求对象),和 view(视图对象)。这些参数是用于执行分页操作所需的基本信息
    def paginate_queryset(self, queryset, request, view=None):
        """
        Paginate a queryset if required, either returning a
        page object, or `None` if pagination is not configured for this view.
        """
        
        # 首先获取每页的数量(page_size),通过调用 self.get_page_size(request) 方法来获取。
        page_size = self.get_page_size(request)
        # 如果 page_size 为 None,表示分页未配置,函数将返回 None,即不进行分页操作
        if not page_size:
            return None
		
        # 创建了一个Django分页器(paginator)对象,使用传入的 queryset 和 page_size 参数。
        # 这将根据查询集的大小和每页的数量创建分页。
        paginator = self.django_paginator_class(queryset, page_size)
        
        # 获取当前请求的页码(page_number),通过调用 self.get_page_number(request, paginator) 方法来获取。
        # 如果请求中的页码是 'last',则页码将设置为最后一页的页码。
        page_number = self.get_page_number(request, paginator)

        try:
            # 尝试使用分页器将查询集分页,即执行实际的分页操作,通过调用 paginator.page(page_number) 方法。
            # 如果分页操作成功,函数将分页后的页面对象(self.page)保存下来,以备后续使用。
            self.page = paginator.page(page_number)
        except InvalidPage as exc:
            # 如果页码无效(例如,超出了分页范围),则会引发 InvalidPage 异常。
            msg = self.invalid_page_message.format(
                page_number=page_number, message=str(exc)
            )
            raise NotFound(msg)
		
        # 如果总页数大于1且模板(template)已经设置,表示有多页数据可供分页
        if paginator.num_pages > 1 and self.template is not None:
            # The browsable API should display pagination controls.
            
            # 于是函数将 display_page_controls 设置为 True,以便在浏览API时显示分页控件。
            self.display_page_controls = True
		
        # 将请求对象保存在 self.request 中,并返回分页后的数据列表,即当前页的数据。
        self.request = request
        
        # 最后,它返回分页后的数据列表。
        return list(self.page)
	
    # 从请求中获取页码
    def get_page_number(self, request, paginator):
        
        # # 从请求中获取页码
        page_number = request.query_params.get(self.page_query_param, 1)
        
        # 如果页码为last_page_strings中的任何一个字符串
        if page_number in self.last_page_strings:
            
            # 则返回最后一页的页码。
            page_number = paginator.num_pages
            
        # 否则则返回默认页码 1
        return page_number
	
    # 根据分页后的数据创建响应,包括总数、下一页和上一页的链接和当前页数据
    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('count', self.page.paginator.count),
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))
	
    # 返回用于响应分页数据的JSON Schema。
    # 返回一个 JSON Schema,用于描述分页响应的结构。JSON Schema 是一种用于验证和描述 JSON 数据结构的规范
    def get_paginated_response_schema(self, schema):
        return {
            # 表示根对象是一个 JSON 对象
            'type': 'object',
            
            # 包含不同属性的字典,描述了响应对象的各个字段
            'properties': {
                
                 # 表示总记录数
                'count': {
                    # 其类型为整数('integer')
                    'type': 'integer',
                    # 并提供一个示例值为 123
                    'example': 123,
                },
                
                # 表示下一页的链接
                'next': {
                    # 其类型为字符串('string')
                    'type': 'string',
                    # 此字段可为空('nullable': True),因为最后一页没有下一页
                    'nullable': True,
                    # 
                    'format': 'uri',
                    # 它还提供了一个示例链接
                    # 包括了 {page_query_param},它将在实际响应中替换为页码查询参数的值。
                    'example': 'http://api.example.org/accounts/?{page_query_param}=4'.format(
                        page_query_param=self.page_query_param)
                },
                # 表示上一页的链接
                'previous': {
                    # 其类型为字符串('string')
                    'type': 'string',
                    # 此字段也可为空,因为第一页没有上一页
                    'nullable': True,
                    'format': 'uri',
                    # 它同样提供了一个示例链接,包括了 {page_query_param},将在实际响应中替换为页码查询参数的值
                    'example': 'http://api.example.org/accounts/?{page_query_param}=2'.format(
                        page_query_param=self.page_query_param)
                },
                
                # 表示分页后的数据结果,它的结构由传入的 schema 参数决定。
                # 这个字段没有提供示例值,因为它的结构取决于实际的数据模型
                'results': schema,
            },
        }
	
    # 从请求中获取每页的数量。
    def get_page_size(self, request):
        # 检查是否启用了 page_size_query_param(即客户端可以通过查询参数来控制每页的数量)。
        # 如果 page_size_query_param 已启用,则进入以下步骤
        if self.page_size_query_param:
            try:
                # 尝试从请求的查询参数中获取每页的数量。
                # 具体来说,它使用 request.query_params 字典来查找与 page_size_query_param 对应的查询参数值。
                # 这里使用了 request.query_params 是因为查询参数通常包含在请求的 URL 中
                # 如果成功获取查询参数的值,函数尝试将其转换为正整数(_positive_int)。这是因为页码数量必须是正整数。
                return _positive_int(
                    request.query_params[self.page_size_query_param],
                    # # 如果成功转换为正整数,则返回该值作为每页的数量,并且启用了严格模式(strict=True)。
                    strict=True,
                    # # 同时,还应用了 cutoff=self.max_page_size
                    # 这表示如果超出了 self.max_page_size 指定的最大页码数量,则会被截断为最大值。
                    cutoff=self.max_page_size
                )
            # 如果转换失败(例如,查询参数不存在或不是整数),则会捕获 KeyError 和 ValueError 异常。
            except (KeyError, ValueError):
                pass
		
        # 否则使用默认的 self.page_size。
        return self.page_size
	
    # 获取下一页的链接。这些链接在响应中提供客户端导航。
    def get_next_link(self):
        # 首先,函数检查当前页是否有下一页,通过调用 self.page.has_next() 来判断。
        if not self.page.has_next():
            # 如果没有下一页,则直接返回 None。
            return None
        
        # 如果当前页有下一页,函数获取当前请求的绝对URL,通过 self.request.build_absolute_uri() 方法获取
        url = self.request.build_absolute_uri()
        
        # 获取下一页的页码,通过调用 self.page.next_page_number() 来获取。这个方法返回下一页的页码
        page_number = self.page.next_page_number()
        
        # 使用 replace_query_param 方法,将当前页的页码查询参数替换为下一页的页码,以生成下一页的链接。
        # 这个链接将用于导航到下一页的数据
        return replace_query_param(url, self.page_query_param, page_number)
	
    # 获取上一页的链接。这些链接在响应中提供客户端导航。
    def get_previous_link(self):
        # 获取上一页的链接,以便在分页响应中提供给客户端进行导航。
        # 检查当前页是否有上一页,通过调用 self.page.has_previous() 来判断。
        if not self.page.has_previous():
            # 如果当前页没有上一页,则返回 None。
            return None
        
        
        # 如果当前页有上一页,函数获取当前请求的绝对URL,通过 self.request.build_absolute_uri() 方法获取。
        url = self.request.build_absolute_uri()
        
        # 获取上一页的页码,通过调用 self.page.previous_page_number() 来获取。这个方法返回上一页的页码。
        page_number = self.page.previous_page_number()
        
        # 如果上一页的页码是1,表示上一页就是第一页
        if page_number == 1:
            # 使用 remove_query_param 方法去除查询参数中的页码查询参数,以生成上一页的链接。
            return remove_query_param(url, self.page_query_param)
        
        # 如果上一页的页码不是1,使用 replace_query_param 方法,将当前页的页码查询参数替换为上一页的页码,以生成上一页的链接。
        # 这个链接将用于导航到上一页的数据。
        return replace_query_param(url, self.page_query_param, page_number)
	
    # 获取用于HTML渲染的上下文信息。它构建一个包含上一页链接、下一页链接和页码链接的字典,并返回这个字典
    def get_html_context(self):
        # 获取当前请求的绝对URL,通过 self.request.build_absolute_uri() 方法获取。
        base_url = self.request.build_absolute_uri()
		
        # 定义了一个嵌套函数 page_number_to_url,用于将页码映射到相应的URL。
        def page_number_to_url(page_number):
            # 如果页码是1,表示当前页是第一页
            if page_number == 1:
                # 调用 remove_query_param 方法去除查询参数中的页码查询参数,生成上一页的URL
                return remove_query_param(base_url, self.page_query_param)
            else:
                # 否则,调用 replace_query_param 方法将当前页的页码查询参数替换为新的页码,生成页码链接。
                return replace_query_param(base_url, self.page_query_param, page_number)
		
        # 获取当前页码和最后一页的页码。这两个值用于生成页码链接
        current = self.page.number
        final = self.page.paginator.num_pages
        
        # 使用 _get_displayed_page_numbers 函数来生成要显示的页码列表,这个列表通常包括当前页及其周围的几个页码。
        page_numbers = _get_displayed_page_numbers(current, final)
        # 当前页链接
        page_links = _get_page_links(page_numbers, current, page_number_to_url)
		
        # 函数返回包含上一页URL、下一页URL和页码链接列表的字典
        return {
            # 上一页 URL 
            'previous_url': self.get_previous_link(),
            # 下一页 URL
            'next_url': self.get_next_link(),
            # 当前页链接
            'page_links': page_links
        }
	
    # 将分页结果渲染成HTML格式。
    def to_html(self):
        # 获取HTML模板,模板路径由 self.template 指定
        template = loader.get_template(self.template)
        # 调用 get_html_context 获取HTML渲染所需的上下文信息
        context = self.get_html_context()
        # 使用模板引擎渲染模板并传递上下文信息,返回渲染后的HTML内容
        return template.render(context)
	
    # 生成用于API Schema的字段描述。它返回一个包含查询参数字段的列表,用于描述分页请求的Schema
    def get_schema_fields(self, view):
        
        # 检查是否安装了 coreapi 和 coreschema,这些是用于生成API Schema的库。
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
        
        # 创建一个 coreapi.Field 对象,用于描述页码查询参数字段。
        fields = [
            # 它包括
            coreapi.Field(
                # 字段的名称
                name=self.page_query_param,
                # 字段是否必须
                required=False,
                # 字段的位置(query)
                location='query',
                # 字段的类型(integer)
                schema=coreschema.Integer(
                    # 字段的标题
                    title='Page',
                    # 字段的描述信息
                    description=force_str(self.page_query_description)
                )
            )
        ]
        
        # 如果分页类还支持页码大小查询参数(self.page_size_query_param 不为 None)
        if self.page_size_query_param is not None:
            
            # 创建一个额外的 coreapi.Field 对象,用于描述页码大小查询参数字段。
            fields.append(
                coreapi.Field(
                    name=self.page_size_query_param,
                    required=False,
                    location='query',
                    schema=coreschema.Integer(
                        title='Page size',
                        description=force_str(self.page_size_query_description)
                    )
                )
            )
            
        # 返回包含字段描述的列表
        return fields
	
    # 用于生成API操作的参数描述。它返回一个包含操作参数描述的列表,用于描述分页请求的参数
    def get_schema_operation_parameters(self, view):
        
        # 创建一个参数字典,包括参数的名称、是否必需、位置(query)、描述等信息。这个字典描述了页码查询参数
        parameters = [
            {
                'name': self.page_query_param,
                'required': False,
                'in': 'query',
                'description': force_str(self.page_query_description),
                'schema': {
                    'type': 'integer',
                },
            },
        ]
        # 如果分页类还支持页码大小查询参数(self.page_size_query_param 不为 None)
        if self.page_size_query_param is not None:
            # # 创建一个额外的参数字典,用于描述页码大小查询参数。
            parameters.append(
                {
                    'name': self.page_size_query_param,
                    'required': False,
                    'in': 'query',
                    'description': force_str(self.page_size_query_description),
                    'schema': {
                        'type': 'integer',
                    },
                },
            )
        # 返回包含参数描述的列表
        return parameters

【六】偏移分页LimitOffsetPagination

  • LimitOffsetPagination:这种分页类使用限制和偏移量来分页,允许你指定返回的结果数量和从哪里开始。

【1】使用

# (2)LimitOffsetPagination:偏移分页
class BookLimitOffsetPagination(LimitOffsetPagination):
    # 重写 4 个 类属性
    default_limit = 2  # 每页显示的条数
    limit_query_param = 'limit'  # limit:3 本页取三条
    offset_query_param = 'offset'  # 偏移量是多少 offset=3&limit:3 : 从第3条开始取3条数据
    max_limit = 5  # 限制每次取的最大条数
  • 自定义分页类:

    • 代码中定义了一个自定义的分页类BookLimitOffsetPagination
    • 它继承自LimitOffsetPagination
  • 在这个类中我们可以重写四个类属性来设置分页的相关参数:

    • default_limit:每页显示的条数,默认值为2。

    • limit_query_param:用于指定每页取多少条数据的查询参数,默认为limit

    • offset_query_param:用于指定偏移量的查询参数,默认为offset

      • 通过设置这个参数,可以使得分页结果实现偏移取值
      • 即从第几条数据开始取,然后取多少条数据。
    • max_limit:限制每次获取的最大条数,默认值为5。

【2】源码分析

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

    http://api.example.org/accounts/?limit=100
    http://api.example.org/accounts/?offset=400&limit=100
    """
    
    # 默认每页返回的数量,默认值为 api_settings.PAGE_SIZE,通常是 API 的默认页大小。
    default_limit = api_settings.PAGE_SIZE
    
    # 用于客户端设置每页数量的查询参数名称,默认为 'limit'。
    limit_query_param = 'limit'
    # 查询参数的描述,默认为 Number of results to return per page.。
    limit_query_description = _('Number of results to return per page.')
    # 用于客户端设置偏移量的查询参数名称,默认为 'offset'
    offset_query_param = 'offset'
    # 查询参数的描述,默认为 'The initial index from which to return the results.'
    offset_query_description = _('The initial index from which to return the results.')
    # 用于限制客户端可以请求的最大每页数量,默认为 None,表示没有最大限制。
    max_limit = None
    #  用于HTML渲染的模板路径,默认为 'rest_framework/pagination/numbers.html'。
    template = 'rest_framework/pagination/numbers.html'
	
    # 分页查询集。它接收查询集、请求对象和视图对象作为参数,执行以下逻辑:
    def paginate_queryset(self, queryset, request, view=None):
        # 获取 limit 和 offset,通过 self.get_limit(request) 和 self.get_offset(request) 方法。
        self.limit = self.get_limit(request)
        if self.limit is None:
            return None
		
        # 获取查询集的总数量 count,通过 self.get_count(queryset) 方法。
        self.count = self.get_count(queryset)
        # 
        self.offset = self.get_offset(request)
        self.request = request
        
        # 如果 count 大于 limit 且定义了模板路径,则标记显示分页控件
        if self.count > self.limit and self.template is not None:
            self.display_page_controls = True
		
        if self.count == 0 or self.offset > self.count:
            return []
        
        # # 否则,返回从查询集中获取的 offset 到 offset + limit 范围内的数据列表
        return list(queryset[self.offset:self.offset + self.limit])
	
    # 这个函数返回用于响应分页数据的字典
    # 包括 count(总数)、next(下一页链接)、previous(上一页链接)和 results(当前页的数据)。
    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('count', self.count),
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))
	
    # 返回一个JSON格式的分页响应模板,该模板描述了分页响应的结构
    def get_paginated_response_schema(self, schema):
        return {
            'type': 'object',
            'properties': {
                # count: 表示结果总数的整数
                'count': {
                    'type': 'integer',
                    'example': 123,
                },
                # next: 表示下一页的URI(统一资源标识符)
                'next': {
                    # 是一个字符串
                    'type': 'string',
                    # 可以为null(可为空)
                    'nullable': True,
                    'format': 'uri',
                    # 该字段描述了下一页的URL,其中包括了分页查询的参数,如offset_param和limit_param。
                    'example': 'http://api.example.org/accounts/?{offset_param}=400&{limit_param}=100'.format(
                        offset_param=self.offset_query_param, limit_param=self.limit_query_param),
                },
                # previous: 表示上一页的URI
                'previous': {
                    # 也是一个字符串
                    'type': 'string',
                    # 可以为null
                    'nullable': True,
                    'format': 'uri',
                    # 类似于next字段,描述了上一页的URL,包括分页查询参数
                    'example': 'http://api.example.org/accounts/?{offset_param}=200&{limit_param}=100'.format(
                        offset_param=self.offset_query_param, limit_param=self.limit_query_param),
                },
                # results: 表示包含实际结果数据的字段。这个字段的结构由参数schema定义,它应该是一个包含实际数据结构的JSON对象。
                'results': schema,
            },
        }
	
    # 该方法用于获取分页查询中的限制参数(即每页返回的结果数量)
    def get_limit(self, request):
        
        # 检查请求中是否包含了limit_query_param指定的参数(通常是limit)
        # 如果存在并且是一个正整数,则返回该值
        if self.limit_query_param:
            try:
                return _positive_int(
                    
                    request.query_params[self.limit_query_param],
                    # # 参数strict=True表示要求限制参数是正整数
                    strict=True,
                    # cutoff=self.max_limit表示限制参数不能超过max_limit的值。
                    cutoff=self.max_limit
                )
            except (KeyError, ValueError):
                pass
		
        # 否则,返回默认值default_limit
        return self.default_limit
	
    # 该方法用于获取分页查询中的偏移参数(即从哪里开始返回结果)。
    def get_offset(self, request):
        try:
            # 从请求中获取offset_query_param指定的参数(通常是offset)
            # 它尝试,如果存在并且是一个正整数,则返回该值
            return _positive_int(
                request.query_params[self.offset_query_param],
            )
        except (KeyError, ValueError):
            # 否则,返回0作为默认值。
            return 0
	
    # 该方法用于生成下一页的链接。
    def get_next_link(self):
        # 如果当前页已经是最后一页或没有更多的数据,它将返回None。
        if self.offset + self.limit >= self.count:
            return None
		
        # 否则,它会构建下一页的URL,并将offset和limit参数更新为下一页的值
        url = self.request.build_absolute_uri()
        url = replace_query_param(url, self.limit_query_param, self.limit)

        offset = self.offset + self.limit
        # 后返回新的URL。
        return replace_query_param(url, self.offset_query_param, offset)
	
    # 该方法用于生成上一页的链接。
    def get_previous_link(self):
        
        # 检查当前页是否是第一页或者offset值是否小于等于0。
        # 如果是,说明没有上一页,直接返回None表示没有上一页链接。
        if self.offset <= 0:
            return None
		
        # 如果当前页不是第一页且offset值大于0,那么就需要构建上一页的链接。
        # 首先,获取当前请求的绝对URL地址,这个URL包含了当前页面的查询参数。
        url = self.request.build_absolute_uri()
        # 接下来,通过调用replace_query_param函数,将当前URL中的limit_query_param参数替换为当前分页器的limit值。
        # 这是因为上一页的链接不应该改变每页的限制数量,只需要更新offset参数。
        url = replace_query_param(url, self.limit_query_param, self.limit)
		
        # 判断如果offset - limit小于等于0,说明上一页的起始位置应该是0
        if self.offset - self.limit <= 0:
            # 因此调用remove_query_param函数移除offset_query_param参数。
            return remove_query_param(url, self.offset_query_param)
		
        # 计算新的offset值,即offset - limit
        # 并使用replace_query_param函数将URL中的offset_query_param参数替换为新的offset值。
        offset = self.offset - self.limit
        # 否则,它会构建上一页的URL,将offset和limit参数更新为上一页的值,然后返回新的URL。
        return replace_query_param(url, self.offset_query_param, offset)
	
    # 构建分页器在HTML页面中的显示。
    def get_html_context(self):
        # 获取当前请求的绝对URL地址,并存储在base_url变量中。这个URL包含了当前页面的查询参数。
        base_url = self.request.build_absolute_uri()
		
        # 检查是否设置了limit参数,
        if self.limit:
            # 如果设置了,就计算当前页码current和最终页码final。
            # 计算当前页码的方式是通过将offset除以limit然后加1,因为页码通常从1开始。
            # 最终页码的计算比较复杂,需要考虑不完全分页的情况,即offset不是limit的整数倍时,可能会有一个额外的页面。
            current = _divide_with_ceil(self.offset, self.limit) + 1

            # The number of pages is a little bit fiddly.
            # We need to sum both the number of pages from current offset to end
            # plus the number of pages up to the current offset.
            # When offset is not strictly divisible by the limit then we may
            # end up introducing an extra page as an artifact.
            final = (
                _divide_with_ceil(self.count - self.offset, self.limit) +
                _divide_with_ceil(self.offset, self.limit)
            )
			
            
            final = max(final, 1)
        else:
            current = 1
            final = 1
		
        # 如果当前页码current大于最终页码final,将current设置为final,以确保当前页码不超过最终页码
        if current > final:
            current = final
		
        # 定义了一个内部函数page_number_to_url,用于将页码转换为相应的URL链接。
        def page_number_to_url(page_number):
            # 如果页码是1
            if page_number == 1:
                # 调用remove_query_param函数移除offset_query_param参数,表示回到第一页。
                return remove_query_param(base_url, self.offset_query_param)
            else:
                # 否则,计算新的offset值
                offset = self.offset + ((page_number - current) * self.limit)
                # 然后调用replace_query_param函数将offset_query_param参数替换为新的offset值,以构建包含指定页码的URL。
                return replace_query_param(base_url, self.offset_query_param, offset)
		
        # 调用_get_displayed_page_numbers函数获取在HTML页面中要显示的页码列表page_numbers
        page_numbers = _get_displayed_page_numbers(current, final)
         # 调用_get_page_links函数生成页码链接列表page_links,传入当前页码、最终页码和页码转换函数。
        page_links = _get_page_links(page_numbers, current, page_number_to_url)
		
        # 返回一个包含上一页URL、下一页URL和页码链接列表的字典,用于HTML渲染分页信息。
        return {
            'previous_url': self.get_previous_link(),
            'next_url': self.get_next_link(),
            'page_links': page_links
        }
        
	# 
    def to_html(self):
        # 通过loader.get_template(self.template)获取到指定模板的模板对象,并存储在template变量中。
        # 这个模板对象将用于渲染HTML页面。
        template = loader.get_template(self.template)
        # 调用self.get_html_context()方法获取HTML渲染上下文,这个上下文包含了分页信息,包括上一页URL、下一页URL和页码链接。
        context = self.get_html_context()
        # 使用获取的模板对象template和上下文context来渲染HTML页面,并返回渲染后的HTML内容。
        return template.render(context)
	
    # 这个方法主要用于确定总共有多少个对象,通常用于计算分页信息中的总记录数。
    def get_count(self, queryset):
        """
        Determine an object count, supporting either querysets or regular lists.
        """
        try:
            # 接受一个查询集或普通列表作为参数,然后尝试使用queryset.count()来获取对象的数量。
            # 如果无法使用count()方法
            return queryset.count()
        
        # 出现AttributeError或TypeError异常,就会捕获
        except (AttributeError, TypeError):
            # 尝试使用len(queryset)来获取对象的数量
            return len(queryset)

    def get_schema_fields(self, view):
        # assert coreapi is not None 和 assert coreschema is not None 这两个断言语句用于检查是否安装了coreapi和coreschema库,因为这两个库用于生成API文档。
        # 如果这两个库未安装,将引发AssertionError异常。
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
        
        # 返回一个包含两个coreapi.Field对象的列表,这两个对象分别代表了API的两个请求参数:limit和offset。
        return [
            # coreapi.Field 用于定义API文档中的一个字段。
            # 在这里,我们定义了两个字段,一个是limit字段,一个是offset字段。
            coreapi.Field(
                # name=self.limit_query_param 和 name=self.offset_query_param 分别指定了这两个字段的名称
                # 这些名称通常对应于API中的查询参数名称,例如?limit=10和?offset=20。
                name=self.limit_query_param,
                # required=False 表示这两个字段是可选的,客户端可以选择是否传递它们。
                required=False,
                # location='query' 指定了这两个字段的位置是查询参数。
                location='query',
                # 指定了这两个字段的数据类型为整数(Integer),并提供了标题(title)和描述(description)信息
                # 这些信息将显示在API文档中。
                schema=coreschema.Integer(
                    title='Limit',
                    description=force_str(self.limit_query_description)
                )
            ),
            coreapi.Field(
                name=self.offset_query_param,
                required=False,
                location='query',
                schema=coreschema.Integer(
                    title='Offset',
                    description=force_str(self.offset_query_description)
                )
            )
        ]

    def get_schema_operation_parameters(self, view):
        # parameters 是一个列表,其中包含了两个字典对象,每个字典对象代表一个操作参数。
        parameters = [
            {
                # 'name': 参数的名称,分别为limit和offset。
                'name': self.limit_query_param,
                # 'required': 参数是否为必需的,这里设置为False,表示这两个参数是可选的。
                'required': False,
                # 'in': 参数的位置,这里设置为query,表示这两个参数位于请求的查询参数中。
                'in': 'query',
                # 'description': 参数的描述,通过force_str(self.limit_query_description) 和 force_str(self.offset_query_description) 获取描述信息。
                'description': force_str(self.limit_query_description),
                # 'schema': 参数的数据类型和格式的定义。在这里,'type' 设置为 'integer',表示参数的数据类型是整数。
                'schema': {
                    'type': 'integer',
                },
            },
            {
                'name': self.offset_query_param,
                'required': False,
                'in': 'query',
                'description': force_str(self.offset_query_description),
                'schema': {
                    'type': 'integer',
                },
            },
        ]
        return parameters

【七】游标分页CursorPagination

  • CursorPagination:这是一种基于游标的分页,适用于需要深度分页的情况,如社交媒体应用。

【1】使用

# (3)CursorPagination:游标分页
# 只能上一页或下一页,但是速度特别快,经常用于APP上
class BookCursorPagination(CursorPagination):
    # 重写3个类属性
    cursor_query_param = 'cursor'  # 查询参数
    page_size = 2  # 每页显示2条
    ordering = 'id'  # 必须是要分页的数据表中的字段,一般是id
  • 定义了一个自定义的分页类BookCursorPagination,它继承自Django Rest Framework提供的CursorPagination类。
  • 该分页类通过设置一些属性来控制分页的行为,其中包括:
    • cursor_query_param: 指定查询参数名,这里设置为cursor,表示通过该参数来指定游标位置。
    • page_size: 指定每页显示的记录数,这里设置为2条。
    • ordering: 指定按照哪个字段排序进行分页,这里设置为id字段。

【2】源码分析

class CursorPagination(BasePagination):
    """
    The cursor pagination implementation is necessarily complex.
    For an overview of the position/offset style we use, see this post:
    https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
    """
    
    # cursor_query_param 和 cursor_query_description:定义了查询参数名称和描述,用于表示游标值。
    cursor_query_param = 'cursor'
    cursor_query_description = _('The pagination cursor value.')
    
    # page_size:定义了每页的默认大小
    page_size = api_settings.PAGE_SIZE
    
    # invalid_cursor_message:定义了无效游标的错误消息
    invalid_cursor_message = _('Invalid cursor')
    
    # ordering:定义了默认的排序方式
    ordering = '-created'
    # template:定义了用于呈现分页控件的模板
    template = 'rest_framework/pagination/previous_and_next.html'

    # Client can control the page size using this query parameter.
    # Default is 'None'. Set to eg 'page_size' to enable usage.
    # page_size_query_param 和 page_size_query_description:定义了查询参数名称和描述,用于表示每页大小。
    page_size_query_param = None
    page_size_query_description = _('Number of results to return per page.')

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

    # The offset in the cursor is used in situations where we have a
    # nearly-unique index. (Eg millisecond precision creation timestamps)
    # We guard against malicious users attempting to cause expensive database
    # queries, by having a hard cap on the maximum possible size of the offset.
    
    # offset_cutoff:定义了游标的最大偏移量,以防止恶意用户发出昂贵的数据库查询
    offset_cutoff = 1000

    def paginate_queryset(self, queryset, request, view=None):
        # 获取请求中的页大小(self.page_size),
        self.page_size = self.get_page_size(request)
        # 如果没有指定页大小则返回None,表示不进行分页
        if not self.page_size:
            return None
		
        # 获取请求的基础URL(self.base_url)以及排序方式(self.ordering)
        self.base_url = request.build_absolute_uri()
        self.ordering = self.get_ordering(request, queryset, view)
		
        # 解码游标(self.cursor):如果请求中包含游标参数,则解码游标值,否则创建一个初始游标。
        self.cursor = self.decode_cursor(request)
        

        # 检查游标是否存在。
        # 如果游标不存在(即self.cursor为None),则创建一个初始游标
        # 其中offset为0,reverse为False,current_position为None。这是游标不存在时的默认设置。
        if self.cursor is None:
            (offset, reverse, current_position) = (0, False, None)
        else:
            (offset, reverse, current_position) = self.cursor

        # Cursor pagination always enforces an ordering.

        # 根据游标分页查询:根据请求中的排序方式和游标信息,对查询集(queryset)进行排序,并根据游标信息进行过滤。
        # 游标分页始终需要按照某种排序方式进行分页,以确保分页结果的一致性。
        if reverse:
            # 根据游标分页的要求强制进行排序。如果reverse为True,则对查询集进行反向排序,以确保按照正确的顺序分页。
            queryset = queryset.order_by(*_reverse_ordering(self.ordering))
        else:
            # 否则,按照正常的排序方式排序
            queryset = queryset.order_by(*self.ordering)

        # If we have a cursor with a fixed position then filter by that.
        
        # 获取分页结果:根据游标信息和页大小,从查询结果中获取一页的数据,同时获取一页后面的一个额外项。
        # 这个额外项用于确定是否有下一页。
        if current_position is not None:
            # 如果游标具有固定位置(即current_position不为None),则根据游标信息添加过滤条件。这是为了确保分页结果正确。
            order = self.ordering[0]
            is_reversed = order.startswith('-')
            order_attr = order.lstrip('-')

            # Test for: (cursor reversed) XOR (queryset reversed)
            # 具体来说,它检查游标的排序方式和查询集的排序方式是否一致
            if self.cursor.reverse != is_reversed:
                # 如果不一致,则使用不同的过滤条件
                kwargs = {order_attr + '__lt': current_position}
            else:
                kwargs = {order_attr + '__gt': current_position}
			
            # 接下来,它执行实际的查询,从查询结果中获取一页的数据。
            queryset = queryset.filter(**kwargs)

        # If we have an offset cursor then offset the entire page by that amount.
        # We also always fetch an extra item in order to determine if there is a
        # page following on from this one.
        	
        # 为了确定是否有下一页,它额外获取一页的数据。这是为了避免在浏览下一页时再次向数据库发出查询请求,从而提高性能。
        results = list(queryset[offset:offset + self.page_size + 1])
        self.page = list(results[:self.page_size])

        # Determine the position of the final item following the page.
        # 最后,它确定是否有下一页(has_following_position为True表示有下一页),以及下一页的位置(following_position)。
        # 这将用于构建下一页的游标。
        if len(results) > len(self.page):
            has_following_position = True
            following_position = self._get_position_from_instance(results[-1], self.ordering)
        else:
            has_following_position = False
            following_position = None
		
        # 如果reverse为True,这表示查询集是反向排序的,因此在返回给用户之前
        if reverse:
            # If we have a reverse queryset, then the query ordering was in reverse
            # so we need to reverse the items again before returning them to the user.
            
            # 需要将self.page中的结果反转,以确保它们按照正确的顺序呈现。
            self.page = list(reversed(self.page))

            # Determine next and previous positions for reverse cursors.
            
            # 根据游标信息和当前位置(current_position)以及偏移量(offset)来确定是否有前一页(has_previous)和后一页(has_next)。
            self.has_next = (current_position is not None) or (offset > 0)
            
            # 如果存在前一页或后一页,还会设置相应的游标位置,以便构建前一页和后一页的游标链接。
            self.has_previous = has_following_position
            if self.has_next:
                # next_position表示下一页的位置
                self.next_position = current_position
            if self.has_previous:
                # previous_position表示前一页的位置。
                self.previous_position = following_position
        else:
            # Determine next and previous positions for forward cursors.
            
            self.has_next = has_following_position
            self.has_previous = (current_position is not None) or (offset > 0)
            if self.has_next:
                self.next_position = following_position
            if self.has_previous:
                self.previous_position = current_position

        # Display page controls in the browsable API if there is more
        # than one page.
        # 如果存在前一页或后一页,并且模板(template)已设置
        if (self.has_previous or self.has_next) and self.template is not None:
            # 将display_page_controls设置为True,这表示在可浏览的API中会显示分页控件,以便用户导航到前一页或后一页
            self.display_page_controls = True
            

        return self.page
	
    # 从HTTP请求中获取每页数据条目数量(分页大小)
    def get_page_size(self, request):
        # 检查是否定义了self.page_size_query_param属性。
        # 这个属性通常用于指定客户端可以在请求中使用的查询参数,以控制每页数据的数量。
        # 如果self.page_size_query_param不为None,则表示你允许客户端通过查询参数来自定义每页数据的数量。
        if self.page_size_query_param:
            try:
                # 如果允许客户端自定义每页数据的数量,它尝试从HTTP请求的查询参数(request.query_params)中获取指定的查询参数的值,该查询参数通常是一个整数,用于指定每页数据的数量。
                # 如果成功获取到查询参数的值,并且该值是一个正整数(通过_positive_int函数进行检查),则返回这个正整数作为每页数据的数量。
                # 同时,它还使用strict=True参数来确保只接受正整数,并使用cutoff=self.max_page_size参数来限制每页数据数量不超过self.max_page_size,以防止客户端请求非常大的分页。
                return _positive_int(
                    request.query_params[self.page_size_query_param],
                    strict=True,
                    cutoff=self.max_page_size
                )
           
        	# 如果无法获取查询参数的值、查询参数的值不是正整数、或者超过了最大允许的每页数据数量(如果有限制),则会捕获KeyError(查询参数不存在)和ValueError(值不是正整数)异常,并继续执行下一步。
            except (KeyError, ValueError):
                pass
	
    	# 如果无法获取有效的查询参数值,或者未定义self.page_size_query_param,则返回默认的每页数据数量,即self.page_size。
        return self.page_size
	
    # 用于生成下一页的链接的方法,该方法会根据当前的分页状态和游标信息生成下一页的链接。
    def get_next_link(self):
        # 检查 self.has_next,这个属性表示是否存在下一页。
        # 如果不存在下一页(self.has_next 为 False),则返回 None,表示没有下一页链接可生成。
        if not self.has_next:
            return None
		
        # 检查分页方向和游标信息,以决定如何生成下一页的链接。
        # 游标分页可以有两个方向:正向和反向(根据排序方向)。
        # 正向表示按照升序排序,反向表示按照降序排序。
        if self.page and self.cursor and self.cursor.reverse and self.cursor.offset != 0:
            # If we're reversing direction and we have an offset cursor
            # then we cannot use the first position we find as a marker.
            # 如果当前是反向分页(self.cursor.reverse 为 True)并且游标的偏移量不为零(self.cursor.offset != 0),则表示当前页数据已经反向排序,且存在游标偏移,因此不能使用第一个位置作为标记位置(marker position)。
            compare = self._get_position_from_instance(self.page[-1], self.ordering)
        else:
            # 否则,使用 self.next_position 作为比较位置(compare position),它表示下一页数据的起始位置。
            compare = self.next_position
            
        # 同时,初始化 offset 为 0,用于跟踪需要跳过的数据项数量。
        offset = 0
	
    	# 遍历当前页的数据项,从最后一个数据项开始向前遍历,
        has_item_with_unique_position = False
        for item in reversed(self.page):
            # 获取每个数据项的位置信息(通过 _get_position_from_instance 方法),并与 compare 进行比较。
            position = self._get_position_from_instance(item, self.ordering)
            # 如果某个数据项的位置与 compare 不相等,说明该位置可以作为标记位置,表示下一页的数据开始。
            if position != compare:
                # The item in this position and the item following it
                # have different positions. We can use this position as
                # our marker.
                # 于是,将 has_item_with_unique_position 设置为 True,并退出遍历
                has_item_with_unique_position = True
                break

            # The item in this position has the same position as the item
            # following it, we can't use it as a marker position, so increment
            # the offset and keep seeking to the previous item.
            
            compare = position
            offset += 1
		
        # 如果遍历完整个当前页,但没有找到唯一位置,表示当前页的数据项位置都相同,此时需要根据不同情况来确定下一页的游标信息
        if self.page and not has_item_with_unique_position:
            # There were no unique positions in the page.
            # 如果当前是第一页且没有上一页,表示已经处于第一页且没有更多的数据了
            # 此时将 offset 设置为 self.page_size(下一页的游标偏移量)
            # 并将 position 设置为 None。
            if not self.has_previous:
                # We are on the first page.
                # Our cursor will have an offset equal to the page size,
                # but no position to filter against yet.
                offset = self.page_size
                position = None
                
            # 如果当前是反向分页,说明当前页是最后一页,但由于反向分页的特性,可能会有额外的数据项需要跳过
            # 此时将 offset 设置为 0,表示下一页的游标从数据的开始位置开始
            # 同时将 position 设置为 self.previous_position,表示下一页的游标位置。
            elif self.cursor.reverse:
                # The change in direction will introduce a paging artifact,
                # where we end up skipping forward a few extra items.
                offset = 0
                position = self.previous_position
                
            # 如果不是以上两种情况,表示在正向分页中,使用游标信息来确定下一页的游标。
            # 将 offset 设置为 self.cursor.offset + self.page_size,表示下一页的游标偏移量为当前游标偏移量加上一页数据的大小,
            # 同时将 position 设置为 self.previous_position,表示下一页的游标位置。
            else:
                # Use the position from the existing cursor and increment
                # it's offset by the page size.
                offset = self.cursor.offset + self.page_size
                position = self.previous_position
		
        # 如果当前页没有数据(not self.page),则将 position 设置为 self.next_position,表示下一页的游标位置
        if not self.page:
            position = self.next_position
		
        # 最后,根据生成的 offset、position 和分页方向(正向)创建一个新的游标对象(Cursor
        # 然后调用 encode_cursor 方法将游标对象编码为游标字符串,并返回生成的下一页链接。
        cursor = Cursor(offset=offset, reverse=False, position=position)
        return self.encode_cursor(cursor)
	
    # 用于生成上一页的链接。上一页的链接通常包含在分页 API 响应中,以便客户端可以方便地请求上一页的数据
    def get_previous_link(self):
        
        # 检查 self.has_previous,这个属性表示是否存在上一页。
        # 如果不存在上一页(self.has_previous 为 False),则返回 None,表示没有上一页链接可生成。
        if not self.has_previous:
            return None
		
        # 检查分页方向和游标信息,以决定如何生成上一页的链接。
        # 游标分页可以有两个方向:正向和反向(根据排序方向)。正向表示按照升序排序,反向表示按照降序排序。
        if self.page and self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
            # If we're reversing direction and we have an offset cursor
            # then we cannot use the first position we find as a marker.
            # 如果当前是正向分页(not self.cursor.reverse 为 True)并且游标的偏移量不为零(self.cursor.offset != 0),则表示当前页数据已经正向排序,且存在游标偏移
            # 因此不能使用第一个位置作为标记位置(marker position)。
            compare = self._get_position_from_instance(self.page[0], self.ordering)
        else:
            # 否则,使用 self.previous_position 作为比较位置(compare position),它表示上一页数据的起始位置。
            compare = self.previous_position
        # 同时,初始化 offset 为 0,用于跟踪需要跳过的数据项数量。
        offset = 0
		
        # 
        has_item_with_unique_position = False
        # 遍历当前页的数据项,从第一个数据项开始向后遍历
        for item in self.page:
            # 获取每个数据项的位置信息(通过 _get_position_from_instance 方法),并与 compare 进行比较。
            position = self._get_position_from_instance(item, self.ordering)
            # 如果某个数据项的位置与 compare 不相等,说明该位置可以作为标记位置,表示上一页的数据开始。
            if position != compare:
                # The item in this position and the item following it
                # have different positions. We can use this position as
                # our marker.
                # 于是,将 has_item_with_unique_position 设置为 True,并退出遍历。
                has_item_with_unique_position = True
                break

            # The item in this position has the same position as the item
            # following it, we can't use it as a marker position, so increment
            # the offset and keep seeking to the previous item.
            
            
            compare = position
            offset += 1
		
        # 如果遍历完整个当前页,但没有找到唯一位置,表示当前页的数据项位置都相同,此时需要根据不同情况来确定上一页的游标信息
        if self.page and not has_item_with_unique_position:
            # There were no unique positions in the page.
            
            # 如果当前是最后一页且没有下一页,表示已经处于最后一页且没有更多的数据了,此时将 offset 设置为 self.page_size(上一页的游标偏移量),并将 position 设置为 None。
            if not self.has_next:
                # We are on the final page.
                # Our cursor will have an offset equal to the page size,
                # but no position to filter against yet.
                offset = self.page_size
                position = None
                
            # 如果当前是反向分页,说明当前页是第一页,但由于反向分页的特性,可能会有额外的数据项需要跳过,此时将 offset 设置为 0,表示上一页的游标从数据的开始位置开始,同时将 position 设置为 self.next_position,表示上一页的游标位置。
            elif self.cursor.reverse:
                # Use the position from the existing cursor and increment
                # it's offset by the page size.
                offset = self.cursor.offset + self.page_size
                position = self.next_position
                
            # 如果不是以上两种情况,表示在正向分页中,使用游标信息来确定上一页的游标。
            # 将 offset 设置为 self.cursor.offset + self.page_size,表示上一页的游标偏移量为当前游标偏移量加上一页数据的大小,同时将 position 设置为 self.next_position,表示上一页的游标位置。
            else:
                # The change in direction will introduce a paging artifact,
                # where we end up skipping back a few extra items.
                offset = 0
                position = self.next_position
		
        # 如果当前页没有数据(not self.page),则将 position 设置为 self.previous_position,表示上一页的游标位置。
        if not self.page:
            position = self.previous_position
		
        # 最后,根据生成的 offset、position 和分页方向(反向)创建一个新的游标对象(Cursor)
        # 然后调用 encode_cursor 方法将游标对象编码为游标字符串,并返回生成的上一页链接。
        cursor = Cursor(offset=offset, reverse=True, position=position)
        return self.encode_cursor(cursor)
	
    # self(Pagination 实例),request(Django 请求对象),queryset(查询集),和 view(Django Rest Framework 视图实例)。
    # 该方法的目标是返回一个可以用于 Django 查询集的排序方式,通常是一个包含字段名的元组。
    def get_ordering(self, request, queryset, view):
        """
        Return a tuple of strings, that may be used in an `order_by` method.
        """
        
        # 定义了一个名为 ordering_filters 的列表,用于存储视图中已定义了 get_ordering 方法的过滤器类。
        # 这些过滤器类通常用于处理视图中的排序逻辑。
        # 过滤器类是根据视图的 filter_backends 属性确定的,如果过滤器类实现了 get_ordering 方法,它就会被包含在 ordering_filters 列表中。
        ordering_filters = [
            filter_cls for filter_cls in getattr(view, 'filter_backends', [])
            if hasattr(filter_cls, 'get_ordering')
        ]
		
        # 检查 ordering_filters 是否存在。
        
        
        if ordering_filters:
            # If a filter exists on the view that implements `get_ordering`
            # then we defer to that filter to determine the ordering.
            
            # 如果存在过滤器类实现了 get_ordering 方法
            # 代码会选择第一个过滤器类(ordering_filters[0])并创建其实例(filter_instance)
            filter_cls = ordering_filters[0]
            filter_instance = filter_cls()
            # 然后调用过滤器的 get_ordering 方法来获取排序方式。
            ordering = filter_instance.get_ordering(request, queryset, view)
            assert ordering is not None, (
                'Using cursor pagination, but filter class {filter_cls} '
                'returned a `None` ordering.'.format(
                    filter_cls=filter_cls.__name__
                )
            )
        else:
            # The default case is to check for an `ordering` attribute
            # on this pagination instance.
            
            # 如果存在过滤器类并成功获取排序方式,则返回这个排序方式。
            # 否则,代码会继续执行默认的排序方式。
            # 默认的排序方式是从 self.ordering 中获取的,其中 self 是分页实例的属性。
            # 如果分页类没有定义 ordering 属性,则会触发一个断言错误,提示需要在分页类上声明排序方式。
            # 这个排序方式通常是一个字符串,表示要按哪个字段排序,或者是一个包含多个字段的元组。
            ordering = self.ordering
            assert ordering is not None, (
                'Using cursor pagination, but no ordering attribute was declared '
                'on the pagination class.'
            )
            assert '__' not in ordering, (
                'Cursor pagination does not support double underscore lookups '
                'for orderings. Orderings should be an unchanging, unique or '
                'nearly-unique field on the model, such as "-created" or "pk".'
            )

        assert isinstance(ordering, (str, list, tuple)), (
            'Invalid ordering. Expected string or tuple, but got {type}'.format(
                type=type(ordering).__name__
            )
        )
		
        # 代码检查排序方式的类型,如果是字符串,则将其封装成一个元组返回,以符合 Django 查询集排序的要求。
        # 如果排序方式已经是元组或列表形式,直接返回
        if isinstance(ordering, str):
            return (ordering,)
        return tuple(ordering)
	
    # 解码请求中的游标,并返回一个 Cursor 实例。游标通常用于标识在查询结果集中的当前位置。
    def decode_cursor(self, request):
        """
        Given a request with a cursor, return a `Cursor` instance.
        """
        # Determine if we have a cursor, and if so then decode it.
        
        # 尝试从请求的查询参数中获取游标信息,查询参数的名称由 self.cursor_query_param 指定。
        # 如果没有找到对应的查询参数,说明客户端没有提供游标,此时返回 None。
        encoded = request.query_params.get(self.cursor_query_param)
        if encoded is None:
            return None

        try:
            # 如果成功获取到游标的编码字符串,代码进一步解码它。
            # 游标通常以某种编码方式进行传输,这里使用了 Base64 编码。
            # 首先,代码通过 b64decode 函数将编码字符串解码成二进制数据,然后将其再次解码成 ASCII 字符串。
            querystring = b64decode(encoded.encode('ascii')).decode('ascii')
            			
            # 解码后的游标字符串通常包含多个部分,如 o(偏移量)、r(是否反向排序)和 p(位置)。
            # 代码使用 Python 的 parse_qs 函数解析这些部分,将它们提取为字典 tokens。
            # keep_blank_values=True 参数表示即使某些部分没有值也要保留键。
            tokens = parse.parse_qs(querystring, keep_blank_values=True)
		
            # 从 tokens 字典中提取游标的各个部分,包括 offset(偏移量)、reverse(是否反向排序)和 position(位置)。
            # 这些部分通常以字符串形式存储。
            offset = tokens.get('o', ['0'])[0]
            # offset 部分表示当前游标的偏移量,通常用于确定查询结果集的起始位置。
            # 代码使用 _positive_int 函数将 offset 转换为正整数,并根据 cutoff 属性来进行截断,以限制最大偏移量的大小。
            offset = _positive_int(offset, cutoff=self.offset_cutoff)
			
            # reverse 部分表示是否应该反向排序查询结果集。
            # 它通常是一个布尔值,代码将其转换为布尔类型。
            reverse = tokens.get('r', ['0'])[0]
            reverse = bool(int(reverse))
			
            # position 部分通常表示游标的当前位置,用于标识在查询结果集中的具体位置。
            # 位置可以是一个字符串或 None,代码直接提取并存储。
            position = tokens.get('p', [None])[0]
        except (TypeError, ValueError):
            # 如果在解码游标的过程中发生了任何异常(如类型错误或数值错误),代码会抛出 NotFound 异常,这表示游标无效。
            raise NotFound(self.invalid_cursor_message)
		
        # 最后,代码使用提取的 offset、reverse 和 position 创建一个 Cursor 实例,并将其返回。
        # Cursor 是一个自定义的数据结构,用于表示游标信息。
        return Cursor(offset=offset, reverse=reverse, position=position)
	
    # 接受一个 Cursor 实例作为参数,该实例包含游标的信息,包括偏移量、反向排序标志和位置
    def encode_cursor(self, cursor):
        """
        Given a Cursor instance, return an url with encoded cursor.
        """
        # 创建一个空字典 tokens,用于存储游标的各个部分
        tokens = {}
        
        # 如果游标的偏移量不为零(即 cursor.offset != 0)
        # 则将偏移量部分添加到 tokens 字典中,使用键 'o'(表示偏移量)和偏移量的字符串表示形式。
        if cursor.offset != 0:
            tokens['o'] = str(cursor.offset)
            
        # 如果游标的反向排序标志为 True(即 cursor.reverse 为 True)
        # 则将反向排序部分添加到 tokens 字典中,使用键 'r' 和值 '1' 表示。
        if cursor.reverse:
            tokens['r'] = '1'
            
        # 如果游标的位置不为 None(即 cursor.position is not None)
        # 则将位置部分添加到 tokens 字典中,使用键 'p' 和位置的字符串表示形式。
        if cursor.position is not None:
            tokens['p'] = cursor.position
		
        # 使用 parse.urlencode 函数将 tokens 字典编码为查询字符串形式的键值对。
        # doseq=True 参数确保多个值具有相同的键时,生成多个键值对。
        querystring = parse.urlencode(tokens, doseq=True)
        
        # 使用 b64encode 函数将查询字符串编码为 ASCII 字符串的 Base64 编码形式。这是为了将游标信息转换为可安全传输的字符串。
        encoded = b64encode(querystring.encode('ascii')).decode('ascii')
        
        # 使用 replace_query_param 函数将编码后的游标字符串添加到请求的 URL 中,以替换原始 URL 中的游标参数。
        # 这确保了响应中的 URL 包含了新的游标信息
        return replace_query_param(self.base_url, self.cursor_query_param, encoded)
	
    # 从数据对象(通常是查询结果的一项)中提取位置信息
    # instance:要从中提取位置信息的数据对象。
    # ordering:表示数据排序方式的元组或字符串。
    def _get_position_from_instance(self, instance, ordering):
        
        # 首先从排序方式 ordering 中提取排序字段的名称(去除可能的负号)
        field_name = ordering[0].lstrip('-')
        if isinstance(instance, dict):
            # 然后根据数据对象 instance 的类型(是否为字典)来提取相应的属性或字典键的值。
            attr = instance[field_name]
        else:
            attr = getattr(instance, field_name)
            
        # 最后,将提取的值转换为字符串,并将其作为位置信息返回。
        return str(attr)

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))
	
    # 构建包含分页信息的响应
    # 接受一个参数 data,它是要包含在响应中的分页数据
    def get_paginated_response_schema(self, schema):
        # 首先构建一个有序字典(OrderedDict),其中包括以下键值对
        return {
            # 
            'type': 'object',
            'properties': {
                # 'next':通过调用 self.get_next_link() 方法获取下一页的链接。
                'next': {
                    'type': 'string',
                    'nullable': True,
                },
                # 'previous':通过调用 self.get_previous_link() 方法获取上一页的链接。
                'previous': {
                    'type': 'string',
                    'nullable': True,
                },
                # 'results':包含实际分页数据的键,即参数 data。
                'results': schema,
            },
        }
	
    # 返回一个字典,其中包含上一页和下一页的链接 URL。
    # 这个方法主要用于构建分页控件的 HTML 上下文数据
    def get_html_context(self):
        return {
            # 'previous_url':通过调用 self.get_previous_link() 方法获取上一页的链接 URL。
            'previous_url': self.get_previous_link(),
            # 'next_url':通过调用 self.get_next_link() 方法获取下一页的链接 URL。
            'next_url': self.get_next_link()
        }
	
    # 生成 HTML 渲染的分页控件内容
    def to_html(self):
        # 首先获取分页模板(self.template)并使用 Django 模板加载器 (loader) 获取模板对象。
        # 然后,它获取上述的 HTML 上下文数据(通过调用 self.get_html_context() 方法),将这些数据传递给模板
        # 最后返回渲染后的 HTML 内容。
        template = loader.get_template(self.template)
        context = self.get_html_context()
        
        # 返回的 HTML 内容通常包括上一页和下一页的链接,以及其他分页控件(如页码导航等),允许用户在浏览器中进行分页导航
        return template.render(context)
	
    # 获取分页器(Paginator)的 schema 字段列表,以便在文档生成和 API 调试时使用
    def get_schema_fields(self, view):
        
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
        # 游标字段(Cursor Field)
        fields = [
            coreapi.Field(
                # name: 字段的名称,通常是 self.cursor_query_param 的值,表示游标参数的名称。
                name=self.cursor_query_param,
                # required: 指示是否必须提供此参数。在这里,游标参数是可选的,因此设置为 False。
                required=False,
                # location: 参数的位置,通常是 'query',表示参数位于查询字符串中。
                location='query',
                # schema: 字段的 schema 描述,用于指定字段的类型和描述信息
                schema=coreschema.String(
                    # title: 字段的标题,通常是 'Cursor',表示游标字段的标题
                    title='Cursor',
                    # description: 字段的描述信息,通常是 'Cursor pagination cursor value',表示游标字段的描述。
                    description=force_str(self.cursor_query_description)
                )
            )
        ]
        # 
        if self.page_size_query_param is not None:
            # 页大小字段(Page Size Field)
            fields.append(
                coreapi.Field(
                    # name: 字段的名称,通常是 self.page_size_query_param 的值,表示页大小参数的名称。
                    name=self.page_size_query_param,
                    # required: 指示是否必须提供此参数。在这里,页大小参数是可选的,因此设置为 False。
                    required=False,
                    # location: 参数的位置,通常是 'query',表示参数位于查询字符串中。
                    location='query',
                    # schema: 字段的 schema 描述,用于指定字段的类型和描述信息
                    schema=coreschema.Integer(
                        # title: 字段的标题,通常是 'Page size',表示页大小字段的标题
                        title='Page size',
                        # description: 字段的描述信息,通常是 'Number of results to return per page',表示页大小字段的描述。
                        description=force_str(self.page_size_query_description)
                    )
                )
            )
        return fields

    def get_schema_operation_parameters(self, view):
        # 游标参数(Cursor Parameter)
        parameters = [
            {
                # name: 参数的名称,通常是 self.cursor_query_param 的值,表示游标参数的名称。
                'name': self.cursor_query_param,
                # required: 指示是否必须提供此参数。在这里,游标参数是可选的,因此设置为 False。
                'required': False,
                # in: 参数的位置,通常是 'query',表示参数位于查询字符串中。
                'in': 'query',
                # description: 参数的描述信息,通常是 'Cursor pagination cursor value',表示游标参数的描述。
                'description': force_str(self.cursor_query_description),
                # schema: 参数的 schema 描述,用于指定参数的类型。在这里,游标参数的类型被设置为 'string',表示它是一个字符串。
                'schema': {
                    'type': 'string',
                },
            }
        ]
        # 页大小参数(Page Size Parameter)
        if self.page_size_query_param is not None:
            parameters.append(
                {
                    # name: 参数的名称,通常是 self.page_size_query_param 的值,表示页大小参数的名称。
                    'name': self.page_size_query_param,
                    # required: 指示是否必须提供此参数。在这里,页大小参数是可选的,因此设置为 False。
                    'required': False,
                    # in: 参数的位置,通常是 'query',表示参数位于查询字符串中。
                    'in': 'query',
                    # description: 参数的描述信息,通常是 'Number of results to return per page',表示页大小参数的描述。
                    'description': force_str(self.page_size_query_description),
                    # schema: 参数的 schema 描述,用于指定参数的类型。在这里,页大小参数的类型被设置为 'integer',表示它是一个整数。
                    'schema': {
                        'type': 'integer',
                    },
                }
            )
        return parameters

【八】自定义分页

【1】自定义分页 ---- 返回全部数据

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

    def get(self, request):

        # 排序条件
        order_param = request.query_params.get('ordering')
        # 过滤条件
        filter_name = request.query_params.get('name')
        book_obj = Book.objects.all()
        if order_param:
            book_obj = book_obj.order_by(order_param)
        if filter_name:
            # 包含过滤条件的被过滤出来
            book_obj = book_obj.filter(name__contains=filter_name)

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

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

        self.back_dict["msg"] = "请求数据成功"
        # self.back_dict["result"] = pagination.get_paginated_response(book_ser.data)

        return pagination.get_paginated_response(book_ser.data)
{
    "count": 6,
    "next": "http://127.0.0.1:8000/app01/v1/books/?limit=2&offset=2",
    "previous": null,
    "results": [
        {
            "id": 1,
            "name": "a",
            "price": 44
        },
        {
            "id": 2,
            "name": "b",
            "price": 666
        }
    ]
}
  • 上述代码实现了一个自定义分页功能,通过 BookLimitOffsetPagination 类对 book_obj 进行分页处理,并返回分页结果。
  • get 方法中,首先获取请求参数中的排序条件 order_param 和过滤条件 filter_name。然后通过 Book.objects.all() 获取所有的 Book 对象。
  • 接下来,根据排序条件和过滤条件对 book_obj 进行排序和过滤操作,得到符合条件的查询结果。
  • 然后,创建 BookLimitOffsetPagination 类的实例 pagination。通过调用 pagination.paginate_queryset(book_obj, request, self) 方法对查询结果进行分页处理,其中 request 是当前请求对象,self 是当前视图对象。
  • 接着,使用序列化器 BookSerializer 对分页后的结果 page 进行序列化,得到序列化后的数据 book_ser
  • 之后,更新 self.back_dict"msg" 键的值为 "请求数据成功"。
  • 最后,通过调用 pagination.get_paginated_response(book_ser.data) 方法,将序列化后的分页数据传入,该方法会返回包含分页信息的字典对象作为响应结果。
  • 综上所述,当访问 {{host}}app01/v1/books/ 时,会返回一个带有分页信息的响应结果,其中 "count" 表示总数,"next" 表示下一页链接,"previous" 表示上一页链接,"results" 表示当前页数据。

【2】自定义分页 ---- 返回自定义数据格式

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

    def get(self, request):

        # 排序条件
        order_param = request.query_params.get('ordering')
        # 过滤条件
        filter_name = request.query_params.get('name')
        book_obj = Book.objects.all()
        if order_param:
            book_obj = book_obj.order_by(order_param)
        if filter_name:
            # 包含过滤条件的被过滤出来
            book_obj = book_obj.filter(name__contains=filter_name)

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

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

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

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

标签:分页,get,self,param,源码,offset,query,page,DRF
From: https://www.cnblogs.com/dream-ze/p/17715149.html

相关文章

  • DRF之排序类源码分析
    【一】排序类介绍在DjangoRESTframework(DRF)中,排序类用于处理API端点的排序操作,允许客户端请求按特定字段对数据进行升序或降序排序。排序类是一种特殊的过滤类DRF提供了内置的排序类,并且你也可以自定义排序类以满足特定的需求。【二】内置排序类OrderingFilterrest_f......
  • DRF之JWT签发Token源码分析
    【一】JWT介绍JWT(JSONWebToken)是一种用于身份认证和授权的开放标准(RFC7519)。它基于JSON格式定义了一种安全的令牌,用于在客户端和服务器之间传输信息。【二】JWT三段式JWT(JSONWebToken)是一种用于身份认证和授权的开放标准(RFC7519)。它基于JSON格式定义了一种安全的令......
  • 短视频app源码,Android TextView文字,删除线以及下划线
    短视频app源码,AndroidTextView文字,删除线以及下划线1、删除线 TextViewtextview=(TextView)view.findViewById(R.id.textviewk);textview.getPaint().setFlags(Paint.STRIKE_THRU_TEXT_FLAG);​2、下划线 TextViewtextview=(TextView)view.findViewById(R.id.textvi......
  • 源码编译Unreal Engine升级到5.3
    1.更新代码gitfetchorigin2.检出5.3.0releasegitcheckout5.3.0release3.安装依赖Setup.bat4.生成项目文件GenerateProjectFiles.bat5.打开UE5.sln编译配置:"DevelopmentEditor","Win64"......
  • PACS医学影像处理系统源码-虚拟内窥镜 三维重建技术
    PACS系统与医院HIS实现双向数据交换,自动从HIS系统获得病人基本信息、检查预约请求,自动向HIS系统传送预约请求接受、检查费用发生信息,接受来自HIS系统的检查结果查询和图像数据检索。医院医学影像PACS系统源码,集成三维影像后处理功能,包括三维多平面重建、三维容积重建、三维表面重建......
  • Glide源码阅读之适配器模式【ArrayAdapterInterface<T>】
    定义菜鸟教程介绍意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。何时使用:1、系统需要使......
  • Glide源码阅读之策略模式2【DownsampleStrategy】
    策略模式二DownsampleStrategy包路径:com.bumptech.glide.load.resource.bitmap.DownsampleStrategy指示对图像进行下采样时使用的算法。DownsampleStrategy不提供任何关于输出大小的保证。行为将不同,取决于ResourceDecoder使用的策略和Android版本的代码运行。使用DownsampleStrat......
  • Glide源码阅读之工厂模式4总结
    工厂模式的应用比较多;变化形态也是各种各样。但经过这段时间的解读。大概可以用浓缩为1、不是使用new创建对象;2、没有明显build方法创建对象;3、带xxxFactory的几乎都满足工厂模式。当然第3点比较明显。如果不带xxxFactory等这样的标识那看看是否有implements、extends。而且内......
  • Glide源码阅读之状态模式[SingleRequest<R>.Status]
    前言前面写完策略模式,接着写状态模式;在开始接触这两个模式的时候我也很疑惑,这两个设计模式很相似,用法也很类似。好一段时间我都没有区分这两者的区别。在使用的时候也不知道怎么选择,后来慢慢的加深理解也就总结出规律了。先看看状态模式的经典结构状态模式介绍《Android源码设计模......
  • Glide源码阅读之策略模式4总结
    《Android源码设计模式解析与实践》定义策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化使用场景针对同一类型问题的多种处理方式。仅仅是具体行为有差别时需要安全地封装多种同一类型的操作时出现同一抽......