Django之优雅的使用分页

Django提供了PaginatorListView两个分页实现类,Paginator实现类支持设置每页大小、获取指定页码的数据,对页码的格式和范围做了容错处理,避免实现分页时编写重复的代码;ListView是对Paginator实现类的封装,使用querysetpaginate_by变量调用Paginator实现类,实现列表视图的分页。

Django中的默认分页可满足大部分的场景,但也存在一些不足,本文将分析Django中分页的实现原理、不足及改进方法。

一、Django中Paginator实现类的用法

打开django_sample项目,进入django_sample目录,创建paginator_example应用

1
$ python manage.py startapp "paginator_example"

向应用paginator_example的视图文件views.py中添加视图UserEndPoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import json

from django.contrib.auth.models import User
from django.core.paginator import Paginator
from django.http import JsonResponse
from django.core import serializers

# Create your views here.
from django.views import View


class UserEndPoint(View):
def get(self, request):
queryset = User.objects.all()

page = request.GET.get('page', 1)
page_size = request.GET.get('pageSize', 10)

# 调用默认分页实现 Paginator
paginator = Paginator(queryset, per_page=page_size)
total = paginator.count
# 获取指定页的数据,结果为queryset
data = paginator.get_page(page)

return JsonResponse({
'status': 201,
'data': json.loads(serializers.serialize('json', data)),
'total': total
})

向应用paginator_example中添加路由文件urls.py并配置UserEndPoint视图的路由

1
2
3
4
5
6
7
from django.urls import path

from paginator_example.views import UserEndPoint

urlpatterns = [
path('users', UserEndPoint.as_view())
]

启动django服务后,访问UserEndPoint视图的地址

访问分页视图

二、Django中Paginator实现类的原理及优缺点

1. 分页实现类的实现原理
分页实现类不仅可以对queryset进行分页,凡事具有count方法、len方法的对象都可以使用Paginator进行分页。

针对queryset分页时,会进行两次数据库的查询,分别查询数据的总量和指定分页的数据;分页的具体实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def get_page(self, number):
"""
Return a valid page, even if the page argument isn't a number or isn't
in range.
"""
try:
number = self.validate_number(number)
except PageNotAnInteger:
number = 1
except EmptyPage:
number = self.num_pages
return self.page(number)

def page(self, number):
"""Return a Page object for the given 1-based page number."""
number = self.validate_number(number)
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
return self._get_page(self.object_list[bottom:top], number, self)

调用get_page方法时,首先检查所获取的分页页码格式是否正确,如果number小于1或大于最大页数,number将被设置为最大页码数,如果number不是整型,将被设置为1,然后调用page方法获取数据;如果直接调用page方法,则需要自己处理number数据有效性验证方法validate_number抛出的异常;

page方法中,首先计算出分页查询条件中的数据,当调用self.count属性时,执行数据的总量的查询语句;然后利用分片queryset进行分片,此时,执行分页查询的sql语句

然后,调用_get_page方法,根据分页后的数据创建Page类的实例化对象,用于后续的操作。

2. Paginator实现类的优点

  • 验证页码和per_page是否为整型,不需要自己做额外的验证和转换
  • 兼容处理:如果当前页码非整形,默认展示第一页;如果当前页码超过最大页,默认展示最后一页
  • 使用简单,方便

3. 🉑️优化方向

  • 增加per_page最大值的限制
  • request对象中获取分页参数,避免外部获取,减少代码冗余

三、Django中Paginator实现类的优化及封装

在应用paginator_example中创建paginator.py文件,编写分页实现类CustomPaginator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.core.paginator import Paginator

from django_sample.settings import PAGINATOR


class CustomPaginator(Paginator):

def __init__(self, request, object_list, per_page=None, orphans=0, allow_empty_first_page=True):
self.request = request
per_page = request.GET.get(PAGINATOR.get('page_size'))
super().__init__(object_list, per_page, orphans, allow_empty_first_page)

def get_page(self):
page = self.request.GET.get(PAGINATOR.get('page'), 1)
if self.per_page > PAGINATOR.get('max_page_size'):
self.per_page = PAGINATOR.get('max_page_size')
return super().get_page(number=page)

上述实现类增加了直接从request对象中获取per_pagepage,减少重复获取页码和每页大小的代码;增加pre_page最大值的验证,避免大量数据查询导致数据库性能下降。

使用上述实现类时,只需要在项目django_samplesettings模块/文件中增加分页配置,如下

1
2
3
4
5
PAGINATOR = {
'max_page_size': 100,
'page': 'page',
'page_size': 'pageSize'
}

然后,在视图中调用分页类CustomPaginator即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import json

from django.contrib.auth.models import User
from django.http import JsonResponse
from django.core import serializers

# Create your views here.
from django.views import View

from paginator_example.paginator import CustomPaginator

class UserV2EndPoint(View):
def get(self, request):
queryset = User.objects.all()
paginator = CustomPaginator(request, queryset)
data = paginator.get_page()

return JsonResponse({
'status': 201,
'data': json.loads(serializers.serialize('json', data)),
})

视图UserV2EndPointUserEndPoint相比,代码量减少,而且更加的清晰。

分页的最佳实践

  1. 分页实现类Paginator中存在page和per_page的数据类型验证,不需要在视图中进行额外的验证
  2. 分页实现类Paginator中未限制每页的最大数量,如果查询的数据量过大,可能导致数据库挂掉,需要在视图中限制每页的最大数量
  3. 可重写视图View,增加分页方法,进一步减少视图文件中的代码量,同时也更加方便后续的维护

四、参考资料

owefsad wechat
进击的DevSecOps,持续分享SAST/IAST/RASP的技术原理及甲方落地实践。如果你对 SAST、IAST、RASP方向感兴趣,可以扫描下方二维码关注公众号,获得更及时的内容推送。
0%