手摸手,带你用Django REST Framework撸接口系列十一(分页篇)

当你的数据库数据量非常大时,如果一次将这些数据查询出来, 必然加大了服务器内存的负载,降低系统的运行速度。一种更好的方式是将数据分段展示给用户。如果用户在展示的分段数据中没有找到自己的内容,可以通过指定页码或翻页的方式查看更多数据,直到找到自己想要的内容为止。

DRF提供的分页类

Django REST Framework提供了3种分页类,接下来我们会分别进行演示。

  • PageNumberPagination:普通分页器。支持用户按?page=3&size=10这种更灵活的方式进行查询,这样用户不仅可以选择页码,还可以选择每页展示数据的数量。通常还需要设置max_page_size这个参数限制每页展示数据的最大数量,以防止用户进行恶意查询(比如size=10000), 这样一页展示1万条数据将使分页变得没有意义。
  • LimitOffsetPagination:偏移分页器。支持用户按?limit=20&offset=100这种方式进行查询。offset是查询数据的起始点,limit是每页展示数据的最大条数,类似于page_size。当你使用这个类时,你通常还需要设置max_limit这个参数来限制展示给用户数据的最大数量。
  • CursorPagination类:加密分页器。这是DRF提供的加密分页查询,仅支持用户按响应提供的上一页和下一页链接进行分页查询,每页的页码都是加密的。使用这种方式进行分页需要你的模型有”created”这个字段,否则你要手动指定ordering排序才能进行使用。

PageNumberPagination

此分页样式接受请求查询参数中的单个数字页码。

设置

全局配置

为了全局启用 PageNumberPagination 样式,请使用以下配置,并根据需要设置 PAGE_SIZE

1
2
3
4
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2
}
局部配置

GenericAPIView 子类中,还可以设置 pagination_class 属性,以在每个视图的基础上选择 PageNumberPagination

Configuration (配置)

PageNumberPagination 类包含许多属性,通过重写其中的一些属性可以修改分页属性

为了设置这些属性,应当重写 PageNumberPagination 类,然后如上所述启用自定义分页类。

  • django_paginator_class —— 要使用的django paginator类。默认为 django.core.paginator.Paginator ,适用于大多数情况。
  • page_size —— 表示每页条数。如果设置,则覆盖 PAGE_SIZE 设置。其默认值与 PAGE_SIZE 设置键相同。
  • page_query_param —— 一个字符串值,指示用于分页控件的查询参数的名称。
  • page_size_query_param —— 如果设置,这是一个字符串值,指示允许客户端根据每个请求设置页面显示条数的查询参数的名称。默认值为 None,表示客户端可能无法控制请求的页面大小(?size=??)。
  • max_page_size —— 如果设置,这是一个数字值,指示允许的最大请求页面条数。此属性仅在设置了 page_size_query_param 时有效。
  • last_page_strings —— 一个字符串值的列表或元组,指示可与page_query_param 一起使用的值,以请求集合中的最后一页。默认为 ('last',)
  • template —— 在可浏览API中呈现分页控件时要使用的模板名称。可以重写以修改呈现样式,或设置为 None 以完全禁用HTML分页控件。默认为 "rest_framework/pagination/numbers.html"

但是如果你希望用户按?page=3&size=10这种更灵活的方式进行查询,你就要进行个性化定制。在实际开发过程中,定制比使用默认的分页类更常见,具体做法如下。

自定义普通分页器

创建自定义分页器

我们自定义了一个MyPageNumberPagination类,该类继承了PageNumberPagination类。我们通过page_size设置了每页默认展示数据的条数,通过page_size_query_param设置了每页size的参数名以及通过max_page_size设置了每个可以展示的最大数据条数。

1
2
3
4
5
6
7
8
9
10
11
from rest_framework.pagination import PageNumberPagination


class MyPageNumberPagination(PageNumberPagination):
"""自定义普通分页器"""
# 每页显示条数
page_size = 2
# 客户端控制每页显示条数的参数名(?size=10)
page_size_query_param = "size"
# 每页允许的最大显示条数
max_page_size = 10
使用自定义的分页类
1
2
3
4
5
6
7
8
9
10
11
12
class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只允许登录用户访问
permission_classes = (IsAuthenticated, )
# 使用自定义分页器
pagination_class = MyPageNumberPagination

def perform_create(self, serializer):
# 将request.user与author绑定。调用create方法时执行如下函数。
serializer.save(author=self.request.user)

展示效果如下:

当然定制分页类不限于指定page_sizemax_page_size这些属性,你还可以改变响应数据的输出格式。比如我们这里希望把next和previous放在一个名为links的key里,我们可以修改MyPageNumberPagination类,重写get_paginated_response方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response


class MyPageNumberPagination(PageNumberPagination):
"""自定义普通分页器"""
# 每页显示条数
page_size = 2
# 客户端控制每页显示条数的参数名(?size=10)
page_size_query_param = "size"
# 每页允许的最大显示条数
max_page_size = 10

def get_paginated_response(self, data):
"""自定义输出格式"""
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'results': data
})

再次测试展示效果:

注意:重写get_paginated_response方法非常有用,你还可以给分页响应数据传递额外的内容,比如code状态码等等。

前面的例子中我们只在单个基于类的视图或视图集中使用到了分页类,你还可以修改settings.py全局使用你自定义的分页类,如下所示。展示效果是一样的,我们就不详细演示了。

1
2
3
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS':'分页器路径.MyPageNumberPagination',
}

LimitOffsetPagination

这种分页样式反映了查找多个数据库记录时使用的语法。客户端包括“limit”和“offset”查询参数。limit指示要返回的项目的最大数目,这相当于其他样式中的 page_size。offset指示查询相对于完整的未分页项的起始位置。

Setup (设置)

全局配置

为了全局启用 LimitOffsetPagination样式,请使用以下配置:

1
2
3
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
}

可选地,您也可以设置 PAGE_SIZE 键。如果还使用了 PAGE_SIZE 参数,那么 limit query参数将是可选的,并且可以被客户端忽略。

局部配置

GenericAPIView 子类中,您还可以设置 pagination_class 属性,以便在每个视图的基础上选择 LimitOffsetPagination

Configuration (配置)

LimitOffsetPagination 类包含许多可以重写以修改分页样式的属性。

为了设置这些属性,需要重写 LimitOffsetPagination 类,然后如上所述启用自定义分页类。

  • default_limit —— 数值,指示在客户端未在查询参数中提供限制时使用的限制。默认为与 PAGE_SIZE 设置键相同的值。
  • limit_query_param —— 字符串值,指示“limit”查询参数的名称。默认为 'limit'
  • offset_query_param —— 字符串值,指示“offset”查询参数的名称。默认为 'offset'
  • max_limit —— 如果设置,这是一个数值,指示客户端可能请求的最大允许限制。默认为 None
  • template —— 在可浏览API中呈现分页控件时要使用的模板名称。可以重写以修改呈现样式,或设置为 None 以完全禁用HTML分页控件。默认为 "rest_framework/pagination/numbers.html"

展示效果如下所示,从第5条数据查起,每页展示2条。

自定义位移分页器

创建自定义分页器

我们自定义了一个MyLimitOffsetPagination类,该类继承了LimitOffsetPagination类。我们通过default_limit设置了每页默认展示数据的条数,通过limit_query_param设置了每页size的参数名以及通过max_limit设置了每个可以展示的最大数据条数。

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
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response


class MyLimitOffsetPagination(LimitOffsetPagination):
"""自定义位移分页器"""
# 默认每页条数
default_limit = 5
# 默认设置每页条数的参数名
limit_query_param = 'limit'
# 设置位移即从第几条开始的参数名
offset_query_param = 'offset'
# 每页限制空护短控制显示的最大条数
max_limit = 10

def get_paginated_response(self, data):
"""自定义输出格式"""
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.count,
'results': data
})
使用自定义的分页类
1
2
3
4
5
6
7
8
9
10
11
12
class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只允许登录用户访问
# permission_classes = (IsAuthenticated, )
# 使用自定义分页器
pagination_class = MyLimitOffsetPagination

def perform_create(self, serializer):
# 将request.user与author绑定。调用create方法时执行如下函数。
serializer.save(author=self.request.user)

展示效果如下:

CursorPagination

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

基于cursor的分页要求结果集中的项具有唯一的、不变的顺序。此顺序通常可能是记录上的创建时间戳,因为它提为分页供了一致的顺序。

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

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

Details and limitations

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

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

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

  • 应当是一个不变的值,例如时间戳、slug或其他在创建时只设置一次的字段。

  • 应当是独一无二的,或者几乎是独一无二的。毫秒精度的时间戳就是一个很好的例子。cursor分页的这种实现使用了一种智能的”position plus offset” 样式,允许它正确地支持不严格唯一的值作为排序。

  • 应当为可强制转化为字符串的不可为null的值。

  • 不应当是浮点数。精度误差容易导致不正确的结果。提示:改用小数。(如果已经有一个浮点数字段,并且必须根据它进行分页,那么这里提供了一个使用小数限制精度的 CursorPagination 子类示例。)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class FloatingPointCursorPagination(CursorPagination):
    __rounding_down = decimal.Context(prec=14, rounding=decimal.ROUND_FLOOR)
    __rounding_up = decimal.Context(prec=14, rounding=decimal.ROUND_CEILING)

    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)

    if isinstance(attr, float):
    # repr gives more precision than str
    # but we still lost some precision just from the postgresql-to-python translation.
    if ordering[0][0] == '-':
    attr = self.__rounding_down.create_decimal_from_float(attr)
    else:
    attr = self.__rounding_up.create_decimal_from_float(attr)

    attr_str = force_text(attr)
    return attr_str
  • 字段应具有数据库索引。

使用不满足这些约束的排序字段通常仍然有效,但是您将失去游标分页的一些好处。

Setup (设置)

全局设置

为了全局启用 CursorPagination 样式,请使用以下配置,根据需要修改 PAGE_SIZE

1
2
3
4
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 100
}
局部配置

GenericAPIView 子类中,还可以设置 pagination_class 属性,以在每个视图的基础上选择 CursorPagination

查看默认渲染视图

为什么会出错误? 使用CursorPagination类需要你的模型里有created这个字段,否则你需要手动指定ordering字段。这是因为CursorPagination类只能对排过序的查询集进行分页展示。我们的Article模型只有create_date字段,没有created这个字段,所以会报错。

Configuration (配置)

CursorPagination 类包含许多可以重写以修改分页样式的属性,。

为了设置这些属性,需要重写 CursorPagination 类,然后如上所述启用自定义分页类。

  • page_size = 表示 PAGE_SIZE 的数值。如果设置,则覆盖页面大小设置。默认为与 PAGE_SIZE 设置键相同的值。
  • cursor_query_param = 字符串值,指示“cursor”查询参数的名称。默认为 'cursor'
  • ordering = 这应当是一个字符串或字符串列表,指示将对其应用基于cursor的分页的字段。例如: ordering='slug'。默认为 -created。也可以通过在视图上使用 OrderingFilter 覆盖此值。
  • template = 在可浏览API中呈现分页控件时要使用的模板的名称。可以重写以修改呈现样式,或设置为“无”以完全禁用HTML分页控件。默认为 "rest_framework/pagination/previous_and_next.html"

自定义加密(游标)分页器

为了解决这个问题,我们需要自定义一个MyCursorPagination类,手动指定按create_date排序, 如下所示:

创建自定义分页器

我们自定义了一个MyCursorPagination类,该类继承了CursorPagination类。我们通过page_size设置了每页默认展示数据的条数,通过page_size_query_param设置了每页size的参数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from rest_framework.pagination import CursorPagination
from rest_framework.response import Response


class MyCursorPagination(CursorPagination):
"""自定义加密(游标)分页器"""
# 默认每页条数
page_size = 2
# 默认设置每页条数的参数名
page_size_query_param = 'page_size'
# 默认游标(页码)参数名
cursor_query_param = 'cursor'
# 默认排序字段
ordering = '-created_time'

def get_paginated_response(self, data):
"""自定义输出格式"""
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'results': data
})
使用自定义的分页类

由于这个ordering字段与模型相关,我们并不推荐全局使用自定义的CursorPagination类,更好的方式是在GenericsAPIView或视图集viewsets中通过pagination_class属性指定,如下所示:

1
2
3
4
5
class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
pagination_class = MyCursorPagination

展示效果如下:

函数类视图中使用分页类

注意pagination_class属性仅支持在genericsAPIView和视图集viewset中配置使用。如果你使用函数或简单的APIView开发API视图,那么你需要对你的数据进行手动分页,一个具体使用例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from rest_framework.pagination import PageNumberPagination

class ArticleView(APIView):
"""
显示所有文章
"""
def get(self, request, format=None):
articles = Article.objects.all()

page = PageNumberPagination() # 产生一个分页器对象
page.page_size = 3 # 默认每页显示的多少条记录
page.page_query_param = 'page' # 默认查询参数名为 page
page.page_size_query_param = 'size' # 前台控制每页显示的最大条数
page.max_page_size = 10 # 后台控制显示的最大记录条数

ret = page.paginate_queryset(articles, request)
serializer = ArticleSerializer(ret, many=True)
return Response(serializer.data)

小结

以上就是关于分页器的详细使用方式,有兴趣的同学可以进入rest_framework.pagination.py看下我们上面讲的三个分页器官方是如何定义的,我们可以参照官方的定义方式,打造出最适合自己的分页器。

Custom pagination styles (自定义分页样式)

为了创建自定义分页序列化程序类,应将 pagination.BasePagination 子类化,并重写 paginate_queryset(self, queryset, request, view=None)get_paginated_response(self, data) 方法:

  • paginate_queryset 方法被传递给初始的queryset,它应该返回一个iterable对象,该对象只包含请求页中的数据。
  • The get_paginated_response m方法传递序列化的页数据,并应返回一个 Response

注意,paginate_queryset 方法可能会在分页实例上设置state,该state (?)稍后可能会由get_paginated_response 方法使用。