DRF分页

Django REST Framework 提供了强大的分页功能,可以帮助你处理大量数据的展示问题。

1、基础概念

分页是将大量数据分割成多个小块(页面)的过程,每个页面包含有限数量的数据项。这样做的好处:

  • 减少单次请求的数据量,提高响应速度
  • 降低客户端内存占用
  • 提供更好的用户体验

分页基类BasePagination:定义分页接口规范

class BasePagination:
    display_page_controls = False

    def paginate_queryset(self, queryset, request, view=None): 
        """
        核心方法1:切割查询集,返回当前页数据
        - 参数:待分页的queryset、请求对象request、视图view
        - 返回:当前页数据(列表/queryset切片),若不分页则返回None
        """
        raise NotImplementedError('paginate_queryset() must be implemented.')

    def get_paginated_response(self, data): 
        """
        核心方法2:生成带分页元信息的响应
        - 参数:当前页数据的序列化结果data
        - 返回:Response对象(包含总条数、上下页链接等)
        """
        raise NotImplementedError('get_paginated_response() must be implemented.')

2、页码分页(PageNumberPagination)

客户端请求时携带page参数(默认第 1 页),服务器返回对应页的数据及分页元信息(总页数、总条数等)。

底SQL:SELECT * FROM 表 LIMIT 10 OFFSET 10(第 2 页,每页 10 条, OFFSET是根据页数计算得出)

特点:

  • 基于页码的分页
  • 客户端可以指定页码和每页数量
  • 提供总页数和总记录数

请求示例:

GET /api/products/?page=2&page_size=15(第 2 页,每页 15 条)

响应示例:


{
    "count": 1023,
    "next": "http://api.example.com/products/?page=3&page_size=15",
    "previous": "http://api.example.com/products/?page=1&page_size=15",
    "results": [
        {"id": 16, "name": "Product 16", ...},
        {"id": 17, "name": "Product 17", ...},
        // ... 共15个产品
    ]
}

核心源码解析

class PageNumberPagination(BasePagination):

    def paginate_queryset(self, queryset, request, view=None):
        self.request = request
        page_size = self.get_page_size(request)
        if not page_size:
            return None

        # 初始化Django的Paginator(负责切割逻辑)
        paginator = self.django_paginator_class(queryset, page_size)
        # 获取客户端请求的页码(默认第1页)
        page_number = self.get_page_number(request, paginator)

        # 切割查询集,返回当前页数据
        self.page = paginator.page(page_number)

        return list(self.page)

    def get_page_number(self, request, paginator):
        # 获取当前页码, 处理特殊值如'last'
        page_number = request.query_params.get(self.page_query_param) or 1
        if page_number in self.last_page_strings:
            page_number = paginator.num_pages
        return page_number

    def get_paginated_response(self, data):
        # 构造包含分页元信息的响应
        return Response({
            'count': self.page.paginator.count,
            'next': self.get_next_link(),
            'previous': self.get_previous_link(),
            'results': data,
        })

    def get_page_size(self, request):
        """
        获取每页显示数量,优先使用客户端指定的值
        """
        if self.page_size_query_param:
            with contextlib.suppress(KeyError, ValueError):
                #  尝试从查询参数获取page_size
                return _positive_int(
                    request.query_params[self.page_size_query_param],
                    strict=True,
                    cutoff=self.max_page_size
                )
        return self.page_size

    def get_next_link(self):
        """
        获取下一页的URL
        """
        if not self.page.has_next():
            return None
        url = self.request.build_absolute_uri()
        page_number = self.page.next_page_number()
        return replace_query_param(url, self.page_query_param, page_number)

    def get_previous_link(self):
        """
        上一页的URL
        """
        if not self.page.has_previous():
            return None
        url = self.request.build_absolute_uri()
        page_number = self.page.previous_page_number()
        if page_number == 1:
            return remove_query_param(url, self.page_query_param)
        return replace_query_param(url, self.page_query_param, page_number)

基础使用


# pagination.py
from rest_framework.pagination import PageNumberPagination

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 10  # 默认每页10条
    page_size_query_param = 'page_size'  # 允许客户端通过?page_size=20自定义每页条数
    max_page_size = 100  # 客户端最大可自定义的每页条数(防止恶意请求)
    page_query_param = 'p'  # 自定义页码参数名(默认是page,这里改为p,即?p=2)


# views.py
from rest_framework.viewsets import ModelViewSet
from .models import Book
from .serializers import BookSerializer
from .pagination import StandardResultsSetPagination

class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    pagination_class = StandardResultsSetPagination  # 指定分页类

3、偏移量分页(LimitOffsetPagination)

offset=10:跳过前 10 条数据,limit=20:取接下来的 20 条数据,等价于 SQL 中的LIMIT 20 OFFSET 10

底层 SQL:SELECT * FROM 表 LIMIT 10 OFFSET 20(从第 20 条开始,取 10 条)

特点:

  • 基于限制和偏移量的分页
  • 客户端可以指定从哪条记录开始和获取多少条记录
  • 适用于需要跳过大量记录的场景

请求示例:


GET /api/products/?limit=15&offset=30

响应示例:

{
    "count": 1023,
    "next": "http://api.example.com/products/?limit=15&offset=45",
    "previous": "http://api.example.com/products/?limit=15&offset=15",
    "results": [
        {"id": 31, "name": "Product 31", ...},
        {"id": 32, "name": "Product 32", ...},
        // ... 共15个产品
    ]
}

核心源码解析


class LimitOffsetPagination(BasePagination):
    # 默认限制数量
    default_limit = api_settings.PAGE_SIZE
    # 查询参数名称
    limit_query_param = 'limit'
    offset_query_param = 'offset'
    # 最大限制数量
    max_limit = None

    def paginate_queryset(self, queryset, request, view=None):
        """
        对查询集进行分页处理
        """
        self.request = request
        # 获取客户端指定的limit(每页条数)
        self.limit = self.get_limit(request)
        if self.limit is None:
            return None
        # 计算总条数
        self.count = self.get_count(queryset)
        # 2. 获取客户端指定offset(偏移量)
        self.offset = self.get_offset(request)
        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 []
        # 应用限制和偏移
        return list(queryset[self.offset:self.offset + self.limit])

    def get_paginated_response(self, data):
        """
        返回包含分页信息的响应
        """
        return Response({
            'count': self.count,
            'next': self.get_next_link(),
            'previous': self.get_previous_link(),
            'results': data
        })

    def get_limit(self, request):
        """
        获取限制数量
        """
        if self.limit_query_param:
            with contextlib.suppress(KeyError, ValueError):
                return _positive_int(
                    request.query_params[self.limit_query_param],
                    strict=True,
                    cutoff=self.max_limit
                )
        return self.default_limit

    def get_offset(self, request):
        """
        获取偏移量
        """
        try:
            return _positive_int(
                request.query_params[self.offset_query_param],
            )
        except (KeyError, ValueError):
            return 0

    def get_next_link(self):
        """
        获取下一页的URL
        """
        if self.offset + self.limit >= self.count:
            return None

        url = self.request.build_absolute_uri()
        url = replace_query_param(url, self.limit_query_param, self.limit)

        offset = self.offset + self.limit
        return replace_query_param(url, self.offset_query_param, offset)

    def get_previous_link(self):
        """
        获取上一页的URL
        """
        if self.offset <= 0:
            return None

        url = self.request.build_absolute_uri()
        url = replace_query_param(url, self.limit_query_param, self.limit)

        if self.offset - self.limit <= 0:
            return remove_query_param(url, self.offset_query_param)

        offset = self.offset - self.limit
        return replace_query_param(url, self.offset_query_param, offset)

基本使用


# pagination.py
from rest_framework.pagination import LimitOffsetPagination

class LimitOffsetStandardPagination(LimitOffsetPagination):
    default_limit = 10  # 默认每页10条
    limit_query_param = 'limit'  # 自定义limit参数名(默认是limit)
    offset_query_param = 'offset'  # 自定义offset参数名(默认是offset)
    max_limit = 100  # 最大每页条数

# views.py
class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    pagination_class = LimitOffsetStandardPagination  # 应用偏移量分页

4、游标分页(CursorPagination)

适合大数据集(10 万 + 条)的高效分页方式,基于 "游标"(cursor)定位,避免页码分页在数据量大时的性能问题(如?page=10000会导致数据库扫描低效)。依赖索引

底层 SQL:SELECT * FROM 表 WHERE id < 100 ORDER BY -id LIMIT 10(基于 ID 定位,利用索引快速查询)。

特点:

  • 只能通过next/previous链接翻页,不支持直接跳转到指定页
  • 基于排序字段(通常是自增 ID 或时间戳)生成游标,查询效率极高
  • 适合 "无限滚动" 场景(如社交媒体的信息流)

请求示例:

GET /api/products/?cursor=cD0yMDIzLTA1LTAxKzIzJTNBNTMlM0E1OCUyQzAyJTNBMTM

响应示例:


{
    "next": "http://api.example.com/products/?cursor=cD0yMDIzLTA1LTAxKzIzJTNBNTMlM0E1OCUyQzAyJTNBMTM",
    "previous": null,
    "results": [
        {"id": 16, "name": "Product 16", "created": "2023-05-01T23:53:58.020013Z", ...},
        {"id": 15, "name": "Product 15", "created": "2023-05-01T22:30:45.123456Z", ...},
        // ... 更多产品
    ]
}

核心源码解析


class CursorPagination(BasePagination):
    # 游标查询参数名称
    cursor_query_param = 'cursor'
     # 每页数量
    page_size = api_settings.PAGE_SIZE
    # 排序字段
    ordering = '-created'

    def paginate_queryset(self, queryset, request, view=None):
         """
        对查询集进行分页处理
        """
        self.request = request

        self.page_size = self.get_page_size(request)
        if not self.page_size:
            return None

        self.base_url = request.build_absolute_uri()
        #  确保查询集按ordering排序(游标依赖排序字段)
        self.ordering = self.get_ordering(request, queryset, view)
        # 解析游标(客户端传入的加密字符串,解密为排序字段值)
        self.cursor = self.decode_cursor(request)
        if self.cursor is None:
            # 首次请求:取前page_size条
            (offset, reverse, current_position) = (0, False, None)
        else:
            # 非首次请求:通过游标定位(核心:用WHERE条件替代OFFSET)
            (offset, reverse, current_position) = self.cursor
        # 如果反向,则反转查询集
        if reverse:
            queryset = queryset.order_by(*_reverse_ordering(self.ordering))
        else:
            queryset = queryset.order_by(*self.ordering)
         # 如果有当前位置,则根据当前位置进行过滤
        if current_position is not None:
            order = self.ordering[0]
            is_reversed = order.startswith('-')
            order_attr = order.lstrip('-')
            # 构建过滤条件
            if self.cursor.reverse != is_reversed:
                # 如果是反向,获取当前位置之前的记录
                kwargs = {order_attr + '__lt': current_position}
            else:
                # 如果是正向,获取当前位置之后的记录
                kwargs = {order_attr + '__gt': current_position}

            queryset = queryset.filter(**kwargs)
        # 获取结果列表
        results = list(queryset[offset:offset + self.page_size + 1])
        self.page = list(results[:self.page_size])
        # ...

        if reverse:
            # 如果是反向,反转结果列表
            # ...
        else:
            # ...

        return self.page

    def get_next_link(self):
        """
        获取下一页的URL
        """
        # ...
        # 编码下一个游标
        cursor = Cursor(offset=offset, reverse=False, position=position)
        return self.encode_cursor(cursor)

    def get_previous_link(self):
        """
        获取上一页的URL
        """
        # ...
        # 编码上一个游标
        cursor = Cursor(offset=offset, reverse=True, position=position)
        return self.encode_cursor(cursor)

    def decode_cursor(self, request):
        """
        解码游标
        """
        # 从请求中获取游标参数
        encoded = request.query_params.get(self.cursor_query_param)
        if encoded is None:
            return None

        try:
            # 对游标进行base64解码
            querystring = b64decode(encoded.encode('ascii')).decode('ascii')
            # 解析查询字符串
            tokens = parse.parse_qs(querystring, keep_blank_values=True)
            # 提取参数
            offset = tokens.get('o', ['0'])[0]
            offset = _positive_int(offset, cutoff=self.offset_cutoff)

            reverse = tokens.get('r', ['0'])[0]
            reverse = bool(int(reverse))

            position = tokens.get('p', [None])[0]
        except (TypeError, ValueError):
            raise NotFound(self.invalid_cursor_message)

        return Cursor(offset=offset, reverse=reverse, position=position)

    def encode_cursor(self, cursor):
        """
        编码游标
        """
        tokens = {}
        if cursor.offset != 0:
            tokens['o'] = str(cursor.offset)
        if cursor.reverse:
            tokens['r'] = '1'
        if cursor.position is not None:
            tokens['p'] = cursor.position
        # 将tokens转换为字符串
        querystring = parse.urlencode(tokens, doseq=True)
        # 对字符串进行base64编码
        encoded = b64encode(querystring.encode('ascii')).decode('ascii')
        return replace_query_param(self.base_url, self.cursor_query_param, encoded)

    def _get_position_from_instance(self, instance, ordering):
        field_name = ordering[0].lstrip('-')
        if isinstance(instance, dict):
            attr = instance[field_name]
        else:
            attr = getattr(instance, field_name)
        return str(attr)

    def get_paginated_response(self, data):
        """
        返回包含分页信息的响应
        """
        return Response({
            'next': self.get_next_link(),
            'previous': self.get_previous_link(),
            'results': data,
        })

基本使用:


# pagination.py
from rest_framework.pagination import CursorPagination

class CursorSetPagination(CursorPagination):
    page_size = 10  # 每页10条
    page_size_query_param = 'page_size'  # 允许客户端自定义每页条数
    max_page_size = 100  # 最大每页条数
    ordering = '-id'  # 排序字段(必须指定,通常用自增ID或创建时间,如'-created_at')
    cursor_query_param = 'cursor'  # 游标参数名(默认是cursor)

# views.py
class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    pagination_class = CursorSetPagination  # 应用游标分页

5、分页全局配置

如果希望所有视图都使用同一种分页方式,可在settings.py中全局配置


# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'myapp.pagination.StandardResultsSetPagination',
    'PAGE_SIZE': 10  # 全局默认每页条数(如果分页类中定义了page_size,会覆盖这里)
}

注意:全局配置后,单个视图可通过pagination_class = None禁用分页。

6、调用流程

6.1 类视图

请求 → list方法 → filter_queryset(过滤) → paginate_queryset(分页切割) → 序列化 → get_paginated_response(构造响应) → 响应


class ListModelMixin:
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
        # 步骤1:获取过滤后的查询集(先过滤,后分页)
        queryset = self.filter_queryset(self.get_queryset())
        # 步骤2:调用分页类切割查询集, 调用GenericAPIView中的paginate_queryset
        page = self.paginate_queryset(queryset)
        if page is not None:
            # 步骤3:序列化当前页数据
            serializer = self.get_serializer(page, many=True)
            # 步骤4:生成带分页信息的响应,调用GenericAPIView中的get_paginated_response
            return self.get_paginated_response(serializer.data)
        # 不分页:直接返回所有数据
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

指定分页类
视图通过pagination_class属性指定分页类,若未指定,会使用settings.py中REST_FRAMEWORK['DEFAULT_PAGINATION_CLASS']的全局配置


class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    pagination_class = StandardResultsSetPagination  # 指定分页类

paginate_queryset和get_paginated_response的调用


class GenericAPIView(APIView):
    def paginate_queryset(self, queryset):
        if self.paginator is None:
            return None
        # 调用分页类的paginate_queryset切割数据
        return self.paginator.paginate_queryset(queryset, self.request, view=self)

    def get_paginated_response(self, data):
        # 调用分页类的get_paginated_response
        return self.paginator.get_paginated_response(data)

6.2 函数视图

请求 → 手动过滤查询集 → 实例化分页类 → paginate_queryset(切割) → 序列化 → get_paginated_response(构造响应) → 响应


# views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Book
from .serializers import BookSerializer
from .pagination import StandardResultsSetPagination  # 自定义分页类

@api_view(['GET'])
def book_list(request):
    # 步骤1:获取查询集(可先过滤)
    queryset = Book.objects.all()
    category = request.query_params.get('category')
    if category:
        queryset = queryset.filter(category=category)  # 先过滤
    
    # 步骤2:实例化分页类
    paginator = StandardResultsSetPagination()
    
    # 步骤3:调用paginate_queryset切割查询集
    page = paginator.paginate_queryset(queryset, request)
    if page is not None:
        # 步骤4:序列化当前页数据
        serializer = BookSerializer(page, many=True)
        # 步骤5:生成带分页信息的响应
        return paginator.get_paginated_response(serializer.data)
    
    # 不分页:直接返回所有数据
    serializer = BookSerializer(queryset, many=True)
    return Response(serializer.data)

posted @ 2025-09-03 16:56  xclic  阅读(17)  评论(0)    收藏  举报