1-drf - 分页

about

django3.2 + djanngorestframework3.12.4 + python3.9 + win10

Django默认的分页器适用于前后端不分离的项目使用,那么针对于前后端分离的项目,我们就要学习restframework提供的分页器来进行分页处理了。

所谓的分页器就是restframework写好的分页类,我们这里只需要学习分页器类的使用以及如何局部配置和全局配置,包括如何自定义分页器类,再加上注意事项也就这些了。

分页器是在有了查询结果(可能经过过滤之后):

  • 如果有配置分页器,那么就走分页器进行分页,再将分页后的数据进行序列化返回给客户端。
  • 如果没有配置分页器,那么直接序列化后返回给客户端。

首先说注意事项:

  • 记得分页前我们应该主动加上排序,对一个有序的结果进行分页才是恰当的。否则你可能会遇到warning提示UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list,当你看拿到这个提示,你就要想着去加排序。
  • 在全局进行分页配置之后,它只对使继承了mixins.ListModelMixingenerics.GenericAPIView 这些有list方法的视图类生效。对于继承了APIView的视图类,我们可以手动的去自定义分页类来进行分页处理,否则不生效。

主要的类:

# 分页基类,实现公共的功能,被下面那几个不同的分页类继承
BasePagination


# 主要的几个分页类
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination
# 根据url上的page参数来进行翻页
PageNumberPagination

# 根据limit和offset参数进行翻页
# LimitOffsetPagination

# 根据游标进行翻页
# CursorPagination

# app分页/大数据量分页

必要的准备

models.py,有203条数据。

from django.db import models


class UserInfo(models.Model):
    user = models.CharField(verbose_name='用户名', max_length=32)
    pwd = models.CharField(verbose_name='密码', max_length=32)
    token = models.CharField(verbose_name='token', max_length=64, null=True, blank=True)

    def __str__(self):
        return self.user

    class Meta:
        ordering = ['id', ]  # 我这里提前加上排序了

api/urls.py

from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter,SimpleRouter
from .views import views, auth_views, page_views,jwt_view

router = SimpleRouter()
router.register(prefix='pg', viewset=page_views.IndexView, basename='pg')
urlpatterns = []

urlpatterns += router.urls

"""
主路由代码长这样:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include("api.urls")),
]
"""

settings.py中的关于REST_FRAMEWORK的配置是空的:

REST_FRAMEWORK = {}

如果有变动,后续会列出来,没列出来的,就没有变动。

PageNumberPagination

先来看全局配置。

全局配置1(不推荐)

主要是settings.py文件:

REST_FRAMEWORK = {
    # 使用PageNumberPagination分页类
	"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    # 每页的数据条数,注意,必须指定PAGE_SIZE,并且必须是大写的PAGE_SIZE,否则不生效
	"PAGE_SIZE": 10
}

此时的views.py长这样:

from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination
from api.models import UserInfo

class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']

class IndexView(ModelViewSet):  # 这个视图类的返回会自动应用上分页
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer

这样配置之后,我们访问:

get请求:http://127.0.0.1:8000/api/pg/
响应结果:
{
    "count": 203,  # 数据的总条数
    "next": "http://127.0.0.1:8000/api/pg/?page=2",  # 下一页地址
    "previous": null,	# 上一页地址,因为我们访问的是首页,所以没有上一页
    "results": [   # 当前的页的数据集
        {
            "id": 1,
            "user": "zhangkai1",
            "pwd": "123"
        },
        ........省略........
        {
            "id": 10,
            "user": "zhangkai10",
            "pwd": "123"
        }
    ]
}

局部配置方式1(不推荐)

这种是结合settings.py和视图类搭配使用。

settings.py文件中只指定page_size,分页类在视图类中,谁用谁配置:

REST_FRAMEWORK = {
	# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
	"PAGE_SIZE": 2,
}

此时的views.py长这样:

from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.decorators import action
from rest_framework.response import Response
from api.models import UserInfo


from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination

class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']

class IndexView(ModelViewSet):
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer
    # 使用上分页类,page_size自动回去settings中找,如果setting中同样不指定page_size,分页器不会起作用
	pagination_class = PageNumberPagination   

请求也是正常发送就行了:

get请求:http://127.0.0.1:8000/api/pg/?page=2

{
    "count": 203,
    "next": "http://127.0.0.1:8000/api/pg/?page=3",
    "previous": "http://127.0.0.1:8000/api/pg/",
    "results": [
        {
            "id": 3,
            "user": "zhangkai3",
            "pwd": "123"
        },
        {
            "id": 4,
            "user": "zhangkai4",
            "pwd": "123"
        }
    ]
}

不过,不建议这种方式,因为单独在配置文件中,配置了PAGE_SIZE参数,而没有配置DEFAULT_PAGINATION_CLASS参数,你Django运行会报WARNINGS提示:

Watching for file changes with StatReloader
Performing system checks...

System check identified some issues:

WARNINGS:
?: (rest_framework.W001) You have specified a default PAGE_SIZE pagination rest_framework setting, without specifying also a DEFAULT_PAGINATION_CLASS.
	HINT: The default for DEFAULT_PAGINATION_CLASS is None. In previous versions this was PageNumberPagination. If you wish to define PAGE_SIZE globally whilst defining pagination_class on a per-view basis you may silence this check.

System check identified 1 issue (0 silenced).
February 21, 2023 - 21:27:43
Django version 3.2, using settings 'drf.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

局部配置方式2(推荐)

这种就是重写PageNumberPagination类,这样的话,我们有些参数就可以自己定制了。

settings.py此时关于分页的啥都不用写了:

REST_FRAMEWORK = {
	# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
	# "PAGE_SIZE": 10,
}

我将自定义的分页类也省事儿写到views.py中,比较便于观察,后期可以单独找个文件存放。

from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.decorators import action
from rest_framework.response import Response
from api.models import UserInfo

from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination


class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']


class MyPageNumberPagination(PageNumberPagination):
    # 默认每页显示的数据条数
	page_size = 2
    
    # 自定义分页参数名称,由默认的page改为下面定义的pg
	page_query_param = 'pg'
    
    # 如果没有下面的参数,那么默认显示的条数由上面的page_size值决定
    # 如果指定了下面的参数,可以指定每页显示的条数
	page_size_query_param = 'pg_size'
    
    # 虽然我们可以自己定义每页显示的条数了,但是也不能想写多少写多少,它必须小于等于max_page_size
    # 如果大于max_page_size值则每页按照max_page_size值来决定
	max_page_size = 10
    
    # 当url上 pg=last,则返回最后一页,默认值就是last
    last_page_strings = ('last', )  # 如果自定义,则必须是元组的形式,否则不生效
	
class IndexView(ModelViewSet):
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer
	pagination_class = MyPageNumberPagination

现在请求中,分页就可以有几种方式了:

# 返回第二页,每页2条数据
http://127.0.0.1:8000/api/pg/?pg=2  

# 返回第二页,每页5条数据
http://127.0.0.1:8000/api/pg/?pg=2&pg_size=5  

# 返回第二页,每页10条数据,pg_size值大于max_page_size,按max_page_size值返回,即10条数据    
http://127.0.0.1:8000/api/pg/?pg=2&pg_size=100	
        
# 返回最后一页
http://127.0.0.1:8000/api/pg/?pg=last

自定义分页器响应结果

如果客户端输入的页码有问题,比如输入的不是数字,或者输入0,或者输入其它字符,我们后端应该进行提示并且返回首页内容。

看看怎么处理,这里的示例用的是PageNumberPagination分页器。

from collections import OrderedDict
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.views import APIView
from rest_framework.serializers import ModelSerializer
from rest_framework.response import Response
from django.core.paginator import InvalidPage
from rest_framework.pagination import (
	PageNumberPagination, LimitOffsetPagination,
	CursorPagination, _positive_int, _divide_with_ceil
)
from api.models import UserInfo


class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']

class MyPageNumberPagination(PageNumberPagination):
	page_size = 2
	page_query_param = 'pg'
	page_size_query_param = 'pg_size'
	max_page_size = 10
	last_page_strings = ('last',)
	msg = {}
	
	def get_page_number(self, request, paginator):
		page_number = request.query_params.get(self.page_query_param, 1)
		# 如果输入的是last,表示要访问最后一页
		if page_number in self.last_page_strings:
			return paginator.num_pages
		
		if not page_number.isdecimal():
			self.msg['msg'] = "输入的页码不是数字,已为你返回首页内容"
			return 1
		
		page_number = int(page_number)
		if page_number < 1:
			self.msg['msg'] = '输入的页码必须大于等于1,已为你返回首页内容'
			return 1
		if page_number > paginator.num_pages:
			self.msg['msg'] = "页码超范围了,没取到值,已为你返回首页内容"
			return 1
		return page_number
		
		
		
	def paginate_queryset(self, queryset, request, view=None):
		"""
		重写分页方法
		"""
		self.msg.clear()

		page_size = self.get_page_size(request)
		if not page_size:
			return None
		paginator = self.django_paginator_class(queryset, page_size)
		page_number = self.get_page_number(request, paginator)
		try:
			# 我们在get_page_number方法中做了一些判断
			self.page = paginator.page(page_number)
		except InvalidPage as exc:
			# 然后再这个try中可能有其他的没考虑到的,都让它返回首页
			self.msg['msg'] = str(exc)
			self.page = paginator.page(1)
		if paginator.num_pages > 1 and self.template is not None:
			self.display_page_controls = True
		self.request = request
		return list(self.page)

	def get_paginated_response(self, data):
		""" 重写响应结果 """
		return Response(OrderedDict([
			('total_page', self.page.paginator.num_pages),  # 经过分页后的总页数
			('total_data', self.page.paginator.count),  # 总的记录条数
			('current_page', self.page.number),  # 当前页的页码
			('next_page_url', self.get_next_link()),
			('previous_page_url', self.get_previous_link()),
			('results', data),  # 分页后的当前页的数据
			('msg', self.msg),  # 页码输入有问题,在这里展示错误信息提示
			('code', 0),  # 返回状态码
		]))


class IndexView(ModelViewSet):
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer
	pagination_class = MyPageNumberPagination

settings.py不用配置分页相关内的参数:

REST_FRAMEWORK = {}

来个测试,比如用户瞎输入:

# 请求
http://127.0.0.1:8000/api/pg/?pg=1aaa

# 响应
{
    "total_page": 102,
    "total_data": 203,
    "current_page": 1,
    "next_page_url": "http://127.0.0.1:8000/api/pg/?pg=2",
    "previous_page_url": null,
    "results": [
        {
            "id": 1,
            "user": "zhangkai1",
            "pwd": "123"
        },
        {
            "id": 2,
            "user": "zhangkai2",
            "pwd": "123"
        }
    ],
    "msg": {
        "msg": "输入的页码不是数字,已为你返回首页内容"
    },
    "code": 0
}

LimitOffsetPagination

全局配置1(不推荐)

settings.py文件:

REST_FRAMEWORK = {
    # 使用LimitOffsetPagination分页类
	# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
    # 每页的数据条数,注意,必须指定PAGE_SIZE,并且必须是大写的PAGE_SIZE,否则不生效
	"PAGE_SIZE": 2
}

此时的views.py长这样:

from collections import OrderedDict
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.response import Response
from django.core.paginator import InvalidPage
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination
from api.models import UserInfo

class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']

class IndexView(ModelViewSet):  # 这个视图类的返回会自动应用上分页
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer

这样配置之后,我们访问:

get请求:http://127.0.0.1:8000/api/pg/
响应结果:
{
    "count": 203,
    "next": "http://127.0.0.1:8000/api/pg/?limit=2&offset=2", 
    "previous": null,
    "results": [
        {
            "id": 1,
            "user": "zhangkai1",
            "pwd": "123"
        },
        {
            "id": 2,
            "user": "zhangkai2",
            "pwd": "123"
        }
    ]
}

 # limit是每页展示的条数,这个可以动态调整,offset是查询数据的开始偏移量,相当于上面PageNumberPagination的page参数,默认 offset
get请求:http://127.0.0.1:8000/api/pg/?limit=2&offset=2

{
    "count": 203,
    "next": "http://127.0.0.1:8000/api/pg/?limit=2&offset=4",
    "previous": "http://127.0.0.1:8000/api/pg/?limit=2",
    "results": [
        {
            "id": 3,
            "user": "zhangkai3",
            "pwd": "123"
        },
        {
            "id": 4,
            "user": "zhangkai4",
            "pwd": "123"
        }
    ]
}

全局配置一看就行了,用的不多,还是看看怎么自己封装,在局部使用。

局部配置方式1(不推荐)

这种是结合settings.py和视图类搭配使用。

settings.py文件中只指定page_size,分页类在视图类中,谁用谁配置:

REST_FRAMEWORK = {
	# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
	# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
	"PAGE_SIZE": 2
}

此时的views.py长这样:

from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.decorators import action
from rest_framework.response import Response
from api.models import UserInfo


from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination

class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']

class IndexView(ModelViewSet):
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer
    # 使用上分页类,page_size自动回去settings中找,如果setting中同样不指定page_size,分页器不会起作用
	pagination_class = PageNumberPagination   

局部配置方式2(推荐)

这种就是重写PageNumberPagination类,这样的话,我们有些参数就可以自己定制了。

settings.py此时关于分页的啥都不用写了:

REST_FRAMEWORK = {
	# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
	# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
	# "PAGE_SIZE": 2
}

我将自定义的分页类也省事儿写到views.py中,比较便于观察,后期可以单独找个文件存放。

from collections import OrderedDict
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.response import Response
from django.core.paginator import InvalidPage
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination
from api.models import UserInfo

class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']


class MyLimitOffsetPagination(LimitOffsetPagination):
	# 从offset开始,展示limit的条数
	default_limit = 2
	# 指定地址栏的limit参数名,默认是limit http://127.0.0.1:8000/api/pg/?limit=2&offset=4
	# 例如指定为 lt 那么地址栏就是 http://127.0.0.1:8000/api/pg/?lt=2&offset=4
	limit_query_param = 'lt'
	
	# 指定偏移量的名字,默认是 offset http://127.0.0.1:8000/api/pg/?limit=2&offset=4
	# 如果指定为 ot 那么地址栏就是 http://127.0.0.1:8000/api/pg/?lt=2&ot=4
	offset_query_param = 'ot'
	
	# limit_query_param参数值范围是小于等于max_limit参数值,否则就以max_limit值为准
	max_limit = 10   # 默认为None

class IndexView(ModelViewSet):
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer
	pagination_class = MyLimitOffsetPagination

现在请求中,分页就可以有几种方式了:

# 从offset为4开始,返回2条数据
GET http://127.0.0.1:8000/api/pg/?lt=2&ot=4

{
    "count": 203,
    "next": "http://127.0.0.1:8000/api/pg/?lt=2&ot=6",
    "previous": "http://127.0.0.1:8000/api/pg/?lt=2&ot=2",
    "results": [
        {
            "id": 5,
            "user": "zhangkai5",
            "pwd": "123"
        },
        {
            "id": 6,
            "user": "zhangkai6",
            "pwd": "123"
        }
    ]
}

自定义分页器响应结果

如果客户端输入的页码有问题,比如输入的不是数字,或者输入0,或者输入其它字符,我们后端应该进行提示并且返回首页内容。

看看LimitOffsetPagination分页类中怎么搞,当然了,思路就是下面的思路,可能不是最优解,可能有bug,毕竟是我一拍脑门子自己搞出来的。

思路还是老套路,重写某些方法,或者增加一些判断。

from collections import OrderedDict
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.response import Response
from django.core.paginator import InvalidPage
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination, _positive_int
from api.models import UserInfo

class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']


class MyLimitOffsetPagination(LimitOffsetPagination):
	# 从offset开始,展示limit的条数
	default_limit = 2
	# 指定地址栏的limit参数名,默认是limit http://127.0.0.1:8000/api/pg/?limit=2&offset=4
	# 例如指定为 lt 那么地址栏就是 http://127.0.0.1:8000/api/pg/?lt=2&offset=4
	limit_query_param = 'limit'
	
	# 指定偏移量的名字,默认是 offset http://127.0.0.1:8000/api/pg/?limit=2&offset=4
	# 如果指定为 ot 那么地址栏就是 http://127.0.0.1:8000/api/pg/?lt=2&ot=4
	offset_query_param = 'offset'
	
	# limit_query_param参数值范围是小于等于max_limit参数值,否则就以max_limit值为准
	max_limit = 10  # 默认为None
	
	# 自定义分页的错误提示
	msg = {"offset": "", "limit": ""}
	
	def get_offset(self, request):
		"""
		对于offset值的各种处理都在这搞定就行了,我想到的就是下面那些判断,如果有bug,自行调整
		"""
		# 首先获取用户输入的offset
		offset = request.query_params.get(self.offset_query_param, None)
		
		# 如果客户端访问的是这个地址:http://127.0.0.1:8000/api/pg/?limit=2
		# 要么是就是首页,要么是用户手动调整了参数,没传offset参数,那么就给它返回首页就行了
		# 也返回首页,因为不带offset
		if offset is None:
			self.msg['offset'] = "首页"
			return 0
		if not offset.isdecimal():  # 输入的不是正整数
			self.msg['offset'] = "输入的offset值非法,返回首页"
			return 0
		offset = int(offset)
		if offset >= self.count:
			self.msg['offset'] = "查询页码超范围,已为你返回首页"
			return 0
		if offset == 0:
			self.msg['offset'] = "当前输入的offset为0,已为你返回首页"
			return 0
		# 其它情况就是用户的正常输入了,正常返回即可
		return offset
	
	def paginate_queryset(self, queryset, request, view=None):
		"""
		这种查询效率也是不高,queryset是所有的记录数,然后通过切片将该当前页的数据切出来,再进行序列化传递给客户端
		"""
		self.msg.clear()  # 清空自定义的消息提示
		self.request = request
		self._queryset = queryset
		self.limit = self.get_limit(request)
		if self.limit is None:  # 没有limit直接不走分页了
			return None
		self.count = self.get_count(queryset)
		# -------------- 接下来,就是对关于分页的各种情况的判断处理了 -------------
		# 表中没有数据
		if self.count == 0:
			self.msg['offset'] = "查询结果为空"
			return []
		# 上面源码 self.limit = self.get_limit(request)
		# 中已经处理好了用户输入limit值大于max_limit的情况,这里只是做下提示
		if int(request.query_params.get(self.limit_query_param, self.default_limit)) > self.max_limit:
			self.msg['limit'] = f'limit值不能大于设定值:{self.max_limit}'
		self.offset = self.get_offset(request)
		if self.count > self.limit and self.template is not None:
			self.display_page_controls = True
		return list(queryset[self.offset:self.offset + self.limit])
	
	@property
	def get_current_page(self):
		# 用户的offset参数输入0、瞎输入、或者本就是首页那么当前页就是第一页
		if self.offset <= 1:
			return 1
		else:
			# 正常页码,就应该是总记录数除以每页条数再加一
			# return self.offset // self.limit + 1
			return _divide_with_ceil(self.offset, self.limit)
	
	def get_paginated_response(self, data):
		""" 重写响应结果 """
		return Response(OrderedDict([
			('total_data', self.count),  # 总的记录条数
			('current_page', self.get_current_page),  # 当前页的页码
			('next_page_url', self.get_next_link()),
			('previous_page_url', self.get_previous_link()),
			('results', data),  # 分页后的当前页的数据
			('msg', self.msg),  # 页码输入有问题,在这里展示错误信息提示
			('code', 0),  # 返回状态码
		]))


class IndexView(ModelViewSet):
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer
	pagination_class = MyLimitOffsetPagination

settings.py不用配置分页相关内的参数:

REST_FRAMEWORK = {}

来个测试,比如用户瞎输入:

# 正常的
GET http://127.0.0.1:8000/api/pg/?limit=2&offset=2
# 响应
{
    "total_data": 203,
    "current_page": 1,
    "next_page_url": "http://127.0.0.1:8000/api/pg/?limit=2&offset=4",
    "previous_page_url": "http://127.0.0.1:8000/api/pg/?limit=2",
    "results": [
        {
            "id": 3,
            "user": "zhangkai3",
            "pwd": "123"
        },
        {
            "id": 4,
            "user": "zhangkai4",
            "pwd": "123"
        }
    ],
    "msg": {},
    "code": 0
}

# 其它情况 
GET http://127.0.0.1:8000/api/pg/?limit=2&offset=ada
{
    "total_data": 203,
    "current_page": 1,
    "next_page_url": "http://127.0.0.1:8000/api/pg/?limit=2&offset=2",
    "previous_page_url": null,
    "results": [
        {
            "id": 1,
            "user": "zhangkai1",
            "pwd": "123"
        },
        {
            "id": 2,
            "user": "zhangkai2",
            "pwd": "123"
        }
    ],
    "msg": {
        "offset": "输入的offset值非法,返回首页"
    },
    "code": 0
}

# 其他情况 
GET http://127.0.0.1:8000/api/pg/?limit=2&offset=1111111111111

{
    "total_data": 203,
    "current_page": 1,
    "next_page_url": "http://127.0.0.1:8000/api/pg/?limit=2&offset=2",
    "previous_page_url": null,
    "results": [
        {
            "id": 1,
            "user": "zhangkai1",
            "pwd": "123"
        },
        {
            "id": 2,
            "user": "zhangkai2",
            "pwd": "123"
        }
    ],
    "msg": {
        "offset": "查询页码超范围,已为你返回首页"
    },
    "code": 0
}

CursorPagination

基于cursor的分页提供了一个不透明的“游标”指示器,客户端可以使用它对结果集进行分页。这种分页样式只显示正向和反向控件,而不允许客户端导航到任意位置。

基于cursor的分页比其他方案更复杂。它还要求结果集呈现固定的顺序,并且不允许客户端任意索引结果集。但是,它确实提供了以下好处:

  • 提供一致的分页视图。当恰当地使用时,CursorPagination 可以确保客户端在翻阅记录时永远不会看到同一个项目两次,即使在分页过程中其他客户端正在插入新的项目时亦是如此。
  • 支持使用非常大的数据集。对于极其庞大的数据集,使用基于offset的分页样式进行分页可能会变得效率低下或无法使用。基于cursor的分页方案具有固定时间的属性,并且不会随着数据集大小的增加而减慢。

正确使用基于cursor的分页需要注意一些细节。你需要考虑你希望该方案适用于什么样的顺序。默认值是按 "-created" 排序。这假设模型实例上必须有一个“created”时间戳字段,并且将呈现一个“timeline”样式的,最新添加的项在前面的分页视图。

可以通过重写分页类中的 'ordering' 属性,或将 OrderingFilter 筛选器类与 CursorPagination 一起使用来修改排序。当与 OrderingFilter 一起使用时,您应该考虑严格限制用户可以按其排序的字段。

正确使用cursor分页应使用有满足以下条件的排序字段:

  • 应当是一个不变的值,例如时间戳、slug或其他在创建时只设置一次的字段。
  • 应当是独一无二的,或者几乎是独一无二的。毫秒精度的时间戳就是一个很好的例子。cursor分页的这种实现使用了一种智能的"position plus offset" 样式,允许它正确地支持不严格唯一的值作为排序。
  • 应当为可强制转化为字符串的不可为null的值。
  • 不应当是浮点数。精度误差容易导致不正确的结果。提示:改用小数。
  • 字段应具有数据库索引。

概括起来一句话:性能强大;只能上下翻页;数据集必须是有序的,排序字段最好加上索引;

所以,这个分页器就不方便全局配置了。

接下来,看下局部使用,也就是我们直接自定义分页器类。

settings.py不用配置分页相关内的参数:

REST_FRAMEWORK = {}

然后来看views.py中怎么搞:

from collections import OrderedDict
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.response import Response
from django.core.paginator import InvalidPage
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination, _positive_int, _divide_with_ceil
from api.models import UserInfo


class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']
class MyCursorPagination(CursorPagination):
	# 必须指定这个参数,默认值是 created
	ordering = "id"
	
	# page_size指定每页的数据条数
	# 默认值是api_settings.PAGE_SIZE 也就是None
	# 如果我们setting指定了PAGE_SIZE,就用那个值,但我们settings中关于分页都不写
	page_size = 2
	
	# 指定 cursor 查询参数的名称。默认为 'cursor'
	# 这是一个加密的游标参数,我们一般没必要改动这个参数和参数值
	# http://127.0.0.1:8000/api/pg/?cursor=cD0y&size=2
	cursor_query_param = 'cursor'
	
	# 指定分页数据条数的参数名
	# http://127.0.0.1:8000/api/pg/?cursor=cD0y&size=2
	page_size_query_param = 'size'
	
	# page_size参数值范围是小于等于 max_page_size 参数值,否则就以 max_page_size 值为准
	max_page_size = 10
	
	# 想搞自定义的话,也是从这里入手,我就不展开说了,参考上面那个MyLimitOffsetPagination中的代码逻辑
	def paginate_queryset(self, queryset, request, view=None):
		# 因为父类的queryset是数据的总数量,我们这里想要获取这个值,可以简单的拿一下,然后剩下的逻辑还是让其父类的方法执行
		self._queryset_size = len(queryset)
		return super(MyCursorPagination, self).paginate_queryset(queryset, request, view=None)
	
	@property
	def get_current_page(self):
		if self.cursor is None:  # 为None时,是首页
			return 1
		else:
			return (int(self.cursor[2]) // self.page_size) + 1
	
	# 自定义响应结果,也是从这里入手
	def get_paginated_response(self, data):
		return Response(OrderedDict([
			('total_data', self._queryset_size),  # 数据总条数
			('total_page', (self._queryset_size // self.page_size) + 1),  # 根据数据总条数和每页数量算出来的总页数
			('current_page', self.get_current_page),  # 当前页的页码
			('next', self.get_next_link()),   # 下一页,如果为null表示没有下一页了
			('previous', self.get_previous_link()),  # 上一页,如果为null表示没有上一页了
			('results', data),  # 当前页码的数据
			('code', 0),  # 自定义返回状态码
		]))


class IndexView(ModelViewSet):
	queryset = UserInfo.objects.all()
	serializer_class = UserInfoSerializer
	pagination_class = MyCursorPagination

来个效果吧:

GET http://127.0.0.1:8000/api/pg/

{
    "total_data": 203,
    "total_page": 102,
    "current_page": 1,
    "next": "http://127.0.0.1:8000/api/pg/?cursor=cD0y",
    "previous": null,
    "results": [
        {
            "id": 1,
            "user": "zhangkai1",
            "pwd": "123"
        },
        {
            "id": 2,
            "user": "zhangkai2",
            "pwd": "123"
        }
    ]
}

# 可以不传size参数
GET http://127.0.0.1:8000/api/pg/?cursor=cD00
{
    "total_data": 203,
    "total_page": 102,
    "current_page": 3,
    "next": "http://127.0.0.1:8000/api/pg/?cursor=cD02",
    "previous": "http://127.0.0.1:8000/api/pg/?cursor=cj0xJnA9NQ%3D%3D",
    "results": [
        {
            "id": 5,
            "user": "zhangkai5",
            "pwd": "123"
        },
        {
            "id": 6,
            "user": "zhangkai6",
            "pwd": "123"
        }
    ],
    "code": 0
}

# 可以传size参数
http://127.0.0.1:8000/api/pg/?cursor=cD00&size=3
{
    "total_data": 203,
    "total_page": 68,
    "current_page": 2,
    "next": "http://127.0.0.1:8000/api/pg/?cursor=cD03&size=3",
    "previous": "http://127.0.0.1:8000/api/pg/?cursor=cj0xJnA9NQ%3D%3D&size=3",
    "results": [
        {
            "id": 5,
            "user": "zhangkai5",
            "pwd": "123"
        },
        {
            "id": 6,
            "user": "zhangkai6",
            "pwd": "123"
        },
        {
            "id": 7,
            "user": "zhangkai7",
            "pwd": "123"
        }
    ],
    "code": 0
}

APIView中使用分页

如果视图类继承了APIView,我们自己写逻辑,该如何使用分页呢,这里我们一起来看下。

这里还是选择settings.py中不写分页相关的配置:

REST_FRAMEWORK = {}

urls.py也要变动下:

from django.urls import path, include, re_path
from .views import views, auth_views, page_views,jwt_view

urlpatterns = [
	path('pg/', page_views.IndexView.as_view()),
]

"""
主路由代码长这样:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include("api.urls")),
]
"""

其它的都是视图类的代码了,我们分开来看。

PageNumberPagination

还是使用我们自定义的分页器类。

代码主要是视图类views.py

from collections import OrderedDict
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.views import APIView
from rest_framework.serializers import ModelSerializer
from rest_framework.response import Response
from django.core.paginator import InvalidPage
from rest_framework.pagination import (
	PageNumberPagination, LimitOffsetPagination,
	CursorPagination, _positive_int, _divide_with_ceil
)
from api.models import UserInfo


class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']


class MyPageNumberPagination(PageNumberPagination):
	page_size = 2
	page_query_param = 'pg'
	page_size_query_param = 'pg_size'
	max_page_size = 10
	last_page_strings = ('last',)
	msg = {}
	
	def get_page_number(self, request, paginator):
		page_number = request.query_params.get(self.page_query_param, 1)
		# 如果输入的是last,表示要访问最后一页
		if page_number in self.last_page_strings:
			return paginator.num_pages
		
		if not page_number.isdecimal():
			self.msg['msg'] = "输入的页码不是数字,已为你返回首页内容"
			return 1
		
		page_number = int(page_number)
		if page_number < 1:
			self.msg['msg'] = '输入的页码必须大于等于1,已为你返回首页内容'
			return 1
		if page_number > paginator.num_pages:
			self.msg['msg'] = "页码超范围了,没取到值,已为你返回首页内容"
			return 1
		return page_number
		
		
	def paginate_queryset(self, queryset, request, view=None):
		"""
		重写分页方法
		"""
		self.msg.clear()

		page_size = self.get_page_size(request)
		if not page_size:
			return None
		paginator = self.django_paginator_class(queryset, page_size)
		page_number = self.get_page_number(request, paginator)
		try:
			# 我们在get_page_number方法中做了一些判断
			self.page = paginator.page(page_number)
		except InvalidPage as exc:
			# 然后再这个try中可能有其他的没考虑到的,都让它返回首页
			self.msg['msg'] = str(exc)
			self.page = paginator.page(1)
		if paginator.num_pages > 1 and self.template is not None:
			self.display_page_controls = True
		self.request = request
		return list(self.page)

	def get_paginated_response(self, data):
		""" 重写响应结果 """
		return Response(OrderedDict([
			('total_page', self.page.paginator.num_pages),  # 经过分页后的总页数
			('total_data', self.page.paginator.count),  # 总的记录条数
			('current_page', self.page.number),  # 当前页的页码
			('next_page_url', self.get_next_link()),
			('previous_page_url', self.get_previous_link()),
			('results', data),  # 分页后的当前页的数据
			('msg', self.msg),  # 页码输入有问题,在这里展示错误信息提示
			('code', 0),  # 返回状态码
		]))

    
class IndexView(APIView):
	def get(self, request):
		# 获取queryset数据集
		queryset = UserInfo.objects.all()
		# 实例化分页对象
		page_obj = MyCursorPagination()
		# 将queryset传进去进行分页处理
		page_queryset = page_obj.paginate_queryset(queryset, request, self)
		# 将分页后的数据进行序列化
		serializer = UserInfoSerializer(instance=page_queryset, many=True)
		# # 如果这么返回,只有当前页的数据
		# return Response(serializer.data)
		
		# 调用咱们自己的自定义分页方法
		return page_obj.get_paginated_response(serializer.data)

访问也是一切如常:

GET http://127.0.0.1:8000/api/pg/?pg=1111

{
    "total_page": 102,
    "total_data": 203,
    "current_page": 1,
    "next_page_url": "http://127.0.0.1:8000/api/pg/?pg=2",
    "previous_page_url": null,
    "results": [
        {
            "id": 1,
            "user": "zhangkai1",
            "pwd": "123"
        },
        {
            "id": 2,
            "user": "zhangkai2",
            "pwd": "123"
        }
    ],
    "msg": {
        "msg": "页码超范围了,没取到值,已为你返回首页内容"
    },
    "code": 0
}

LimitOffsetPagination

还是使用我们自定义的分页器类。

代码主要是视图类views.py

from collections import OrderedDict
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.views import APIView
from rest_framework.serializers import ModelSerializer
from rest_framework.response import Response
from django.core.paginator import InvalidPage
from rest_framework.pagination import (
	PageNumberPagination, LimitOffsetPagination,
	CursorPagination, _positive_int, _divide_with_ceil
)
from api.models import UserInfo


class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']


class MyLimitOffsetPagination(LimitOffsetPagination):
	# 从offset开始,展示limit的条数
	default_limit = 2
	# 指定地址栏的limit参数名,默认是limit http://127.0.0.1:8000/api/pg/?limit=2&offset=4
	# 例如指定为 lt 那么地址栏就是 http://127.0.0.1:8000/api/pg/?lt=2&offset=4
	limit_query_param = 'limit'

	# 指定偏移量的名字,默认是 offset http://127.0.0.1:8000/api/pg/?limit=2&offset=4
	# 如果指定为 ot 那么地址栏就是 http://127.0.0.1:8000/api/pg/?lt=2&ot=4
	offset_query_param = 'offset'

	# limit_query_param参数值范围是小于等于max_limit参数值,否则就以max_limit值为准
	max_limit = 10  # 默认为None

	# 自定义分页的错误提示
	msg = {"offset": "", "limit": ""}

	def get_offset(self, request):
		"""
		对于offset值的各种处理都在这搞定就行了,我想到的就是下面那些判断,如果有bug,自行调整
		"""
		# 首先获取用户输入的offset
		offset = request.query_params.get(self.offset_query_param, None)

		# 如果客户端访问的是这个地址:http://127.0.0.1:8000/api/pg/?limit=2
		# 要么是就是首页,要么是用户手动调整了参数,没传offset参数,那么就给它返回首页就行了
		# 也返回首页,因为不带offset
		if offset is None:
			self.msg['offset'] = "首页"
			return 0
		if not offset.isdecimal():  # 输入的不是正整数
			self.msg['offset'] = "输入的offset值非法,返回首页"
			return 0
		offset = int(offset)
		if offset >= self.count:
			self.msg['offset'] = "查询页码超范围,已为你返回首页"
			return 0
		if offset == 0:
			self.msg['offset'] = "当前输入的offset为0,已为你返回首页"
			return 0
		# 其它情况就是用户的正常输入了,正常返回即可
		return offset

	def paginate_queryset(self, queryset, request, view=None):
		"""
		这种查询效率也是不高,queryset是所有的记录数,然后通过切片将该当前页的数据切出来,再进行序列化传递给客户端
		"""
		self.msg.clear()  # 清空自定义的消息提示
		self.request = request
		self._queryset = queryset
		self.limit = self.get_limit(request)
		if self.limit is None:  # 没有limit直接不走分页了
			return None
		self.count = self.get_count(queryset)
		# -------------- 接下来,就是对关于分页的各种情况的判断处理了 -------------
		# 表中没有数据
		if self.count == 0:
			self.msg['offset'] = "查询结果为空"
			return []
		# 上面源码 self.limit = self.get_limit(request)
		# 中已经处理好了用户输入limit值大于max_limit的情况,这里只是做下提示
		if int(request.query_params.get(self.limit_query_param, self.default_limit)) > self.max_limit:
			self.msg['limit'] = f'limit值不能大于设定值:{self.max_limit}'
		self.offset = self.get_offset(request)
		if self.count > self.limit and self.template is not None:
			self.display_page_controls = True
		return list(queryset[self.offset:self.offset + self.limit])

	@property
	def get_current_page(self):
		# 用户的offset参数输入0、瞎输入、或者本就是首页那么当前页就是第一页
		if self.offset <= 1:
			return 1
		else:
			# 正常页码,就应该是总记录数除以每页条数再加一
			# return self.offset // self.limit + 1
			return _divide_with_ceil(self.offset, self.limit)

	def get_paginated_response(self, data):
		""" 重写响应结果 """
		return Response(OrderedDict([
			('total_data', self.count),  # 总的记录条数
			('current_page', self.get_current_page),  # 当前页的页码
			('next_page_url', self.get_next_link()),
			('previous_page_url', self.get_previous_link()),
			('results', data),  # 分页后的当前页的数据
			('msg', self.msg),  # 页码输入有问题,在这里展示错误信息提示
			('code', 0),  # 返回状态码
		]))


class IndexView(APIView):
	def get(self, request):
		# 获取queryset数据集
		queryset = UserInfo.objects.all()
		# 实例化分页对象
		page_obj = MyLimitOffsetPagination()
		# 将queryset传进去进行分页处理
		page_queryset = page_obj.paginate_queryset(queryset, request, self)
		# 将分页后的数据进行序列化
		serializer = UserInfoSerializer(instance=page_queryset, many=True)
		# # 如果这么返回,只有当前页的数据
		# return Response(serializer.data)
		
		# 调用咱们自己的自定义分页方法
		return page_obj.get_paginated_response(serializer.data)

访问也没问题:

GET http://127.0.0.1:8000/api/pg/?limit=2&offset=2

{
    "total_data": 203,
    "current_page": 1,
    "next_page_url": "http://127.0.0.1:8000/api/pg/?limit=2&offset=4",
    "previous_page_url": "http://127.0.0.1:8000/api/pg/?limit=2",
    "results": [
        {
            "id": 3,
            "user": "zhangkai3",
            "pwd": "123"
        },
        {
            "id": 4,
            "user": "zhangkai4",
            "pwd": "123"
        }
    ],
    "msg": {},
    "code": 0
}

CursorPagination

还是使用我们自定义的分页器类。

代码主要是视图类views.py

from collections import OrderedDict
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.views import APIView
from rest_framework.serializers import ModelSerializer
from rest_framework.response import Response
from django.core.paginator import InvalidPage
from rest_framework.pagination import (
	PageNumberPagination, LimitOffsetPagination,
	CursorPagination, _positive_int, _divide_with_ceil
)
from api.models import UserInfo


class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']

class MyCursorPagination(CursorPagination):
	# 必须指定这个参数,默认值是 created
	ordering = "id"
	
	# page_size指定每页的数据条数
	# 默认值是api_settings.PAGE_SIZE 也就是None
	# 如果我们setting指定了PAGE_SIZE,就用那个值,但我们settings中关于分页都不写
	page_size = 2
	
	# 指定 cursor 查询参数的名称。默认为 'cursor'
	# 这是一个加密的游标参数,我们一般没必要改动这个参数和参数值
	# http://127.0.0.1:8000/api/pg/?cursor=cD0y&size=2
	cursor_query_param = 'cursor'
	
	# 指定分页数据条数的参数名
	# http://127.0.0.1:8000/api/pg/?cursor=cD0y&size=2
	page_size_query_param = 'size'
	
	# page_size参数值范围是小于等于 max_page_size 参数值,否则就以 max_page_size 值为准
	max_page_size = 10
	
	# 想搞自定义的话,也是从这里入手,我就不展开说了,参考上面那个MyLimitOffsetPagination中的代码逻辑
	def paginate_queryset(self, queryset, request, view=None):
		# 因为父类的queryset是数据的总数量,我们这里想要获取这个值,可以简单的拿一下,然后剩下的逻辑还是让其父类的方法执行
		self._queryset_size = len(queryset)
		return super(MyCursorPagination, self).paginate_queryset(queryset, request, view=None)
	
	@property
	def get_current_page(self):
		if self.cursor is None:  # 为None时,是首页
			return 1
		else:
			return (int(self.cursor[2]) // self.page_size) + 1
	
	# 自定义响应结果,也是从这里入手
	def get_paginated_response(self, data):
		return Response(OrderedDict([
			('total_data', self._queryset_size),  # 数据总条数
			('total_page', (self._queryset_size // self.page_size) + 1),  # 根据数据总条数和每页数量算出来的总页数
			('current_page', self.get_current_page),  # 当前页的页码
			('next', self.get_next_link()),   # 下一页,如果为null表示没有下一页了
			('previous', self.get_previous_link()),  # 上一页,如果为null表示没有上一页了
			('results', data),  # 当前页码的数据
			('code', 0),  # 返回状态码
		]))

class IndexView(APIView):
	def get(self, request):
		# 获取queryset数据集
		queryset = UserInfo.objects.all()
		# 实例化分页对象
		page_obj = MyCursorPagination()
		# 将queryset传进去进行分页处理
		page_queryset = page_obj.paginate_queryset(queryset, request, self)
		# 将分页后的数据进行序列化
		serializer = UserInfoSerializer(instance=page_queryset, many=True)
		# # 如果这么返回,只有当前页的数据
		# return Response(serializer.data)
		
		# 调用咱们自己的自定义分页方法
		return page_obj.get_paginated_response(serializer.data)

访问下:

GET http://127.0.0.1:8000/api/pg/?cursor=cD0y

{
    "total_data": 203,
    "total_page": 102,
    "current_page": 2,
    "next": "http://127.0.0.1:8000/api/pg/?cursor=cD00",
    "previous": "http://127.0.0.1:8000/api/pg/?cursor=cj0xJnA9Mw%3D%3D",
    "results": [
        {
            "id": 3,
            "user": "zhangkai3",
            "pwd": "123"
        },
        {
            "id": 4,
            "user": "zhangkai4",
            "pwd": "123"
        }
    ],
    "code": 0
}

常见报错

UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list:

这个不算报错,只是算是提示,因为在分页时,我们应该对数据进行排序,这样保证分页数据不会出现紊乱问题,所以,如果没有进行排序,就会提示这个Warning。

那么解决办法也是非常简单,加上排序呗。而加排序可以有两种方案。

方案1:在orm中进行排序

这个方案也就是在视图类中,加上排序。

# views.py
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.decorators import action
from rest_framework.response import Response
from api.models import UserInfo


from rest_framework.pagination import PageNumberPagination

class UserInfoSerializer(ModelSerializer):
	class Meta:
		model = UserInfo
		exclude = ['token']

class IndexView(ModelViewSet):
	# queryset = UserInfo.objects.all()
	# 加上order_by排序防止出现warning  UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list:
	queryset = UserInfo.objects.all().order_by('id')
	serializer_class = UserInfoSerializer

方案2:就是在模型类中加上排序

# models.py
from django.db import models


class UserInfo(models.Model):
    user = models.CharField(verbose_name='用户名', max_length=32)
    pwd = models.CharField(verbose_name='密码', max_length=32)
    token = models.CharField(verbose_name='token', max_length=64, null=True, blank=True)

    def __str__(self):
        return self.user

    class Meta:
        # 就下面这行
        ordering = ['id', ]
posted @ 2017-11-20 17:07  听雨危楼  阅读(604)  评论(0编辑  收藏  举报