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)
浙公网安备 33010602011771号