Skip to content

about

django4.2

本篇主要介绍Django4.2版本中, 内置的分页组件的使用,以及基于内置组件的二次开发和自定义分页的实现。

伪造数据

表结构,apps/mypage/models.py

python
from django.db import models

class UserInfo(models.Model):
    num = models.CharField(max_length=32, verbose_name='序号', default=0)
    username = models.CharField(max_length=32, verbose_name='用户名')
    phone = models.CharField(max_length=11, verbose_name='手机号')
    email = models.EmailField(verbose_name='邮箱')

    def __str__(self):
        return self.username

脚本文件中script/test.py

python
# -*- coding = utf-8 -*-
import os
import django
import faker


os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dj42.settings")  # dj42:项目名称
django.setup()  # 这一步就加载了Django环境

# 必须是django.setup()之后,才能引入模型类
from apps.mypage.models import UserInfo

fk = faker.Faker(locale='zh_CN')  # locale='zh_CN' 中文  locale='en_US' 默认美式英文

def foo():
    data_list = [
        UserInfo(num=i, username=fk.name(), phone=fk.phone_number(), email=fk.email())
        for i in range(1, 1000000)
    ]
    UserInfo.objects.bulk_create(data_list)

if __name__ == '__main__':
    foo()

构造好的数据:

1832669366794059776.png

内置分页器类Paginator

官档:https://docs.djangoproject.com/zh-hans/4.2/topics/pagination/

所有分页方法都使用Paginator 类。它完成了将 QuerySet 拆分为 Page 对象的所有繁重工作。

将来我们根据业务需求需要自定义分页器类,也要基层这个Paginator 类,所以我们有必要了解它。

相关方法和属性

为了演示方便,我在脚本中执行的代码:

python
# -*- coding = utf-8 -*-
import os
import django
 

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dj42.settings")  # MB:项目名称
django.setup()  # 这一步就加载了Django环境

# 必须是django.setup()之后,才能引入模型类
from apps.mypage.models import UserInfo

from django.core.paginator import Paginator

def foo():
    paginate_by = 5  # 每页展示的记录条数
    user_queryset = UserInfo.objects.order_by('id')[:100]
    
    # 拿到分页器对象
    pg_obj = Paginator(user_queryset, paginate_by)
    # print(pg_obj)  # <django.core.paginator.Paginator object at 0x000001E9628EEB00>
    
    # 数据总条数
    # print(pg_obj.count)  # 100  总共100条数据要进行分页
    
    # 总分页数,你也能用这个值表示最后一页;那首页就不用说了,写死一个1就行了
    # print(pg_obj.num_pages)  # 20  一共能分20页

    # 每页展示的数据条数
    # print(pg_obj.per_page)  # 5

    # 返回的是总分页数的返回对象,因为当前示例分了20页,所以页码范围就是 range(1, 21)
    # print(pg_obj.page_range)  # range(1, 21)


    # 拿到指定页码的页码对象,可以从这个对象上面获取我们想要的数据
    page1 = pg_obj.page(1)
    # print(page1, type(page1))  # <Page 1 of 20> <class 'django.core.paginator.Page'>

    # 注意,页码对象点paginator能反向拿到分页器对象,其作用就是在模板语言中如果需要分页器对象,能方便的通过paginator调用
    # 注意,页码对象和分页器对象是不要弄混了,这俩不是一个东西
    # print(page1.paginator)  # <django.core.paginator.Paginator object at 0x000001E9628EEB00>
    # print(page1.paginator.per_page)  # 5
    # 获取指定页码的记录,返回的是queryset对象
    # print(page1.object_list)  # <QuerySet [<UserInfo: 1-刘玉梅>, ...., <UserInfo: 4-宋强>, <UserInfo: 5-范健>]>

    # 获取下一页的页码
    # print(page1.next_page_number())  # 2

    # 当前页是否有下一页,有的话返回True,否则返回False
    # print(page1.has_next())  # True

    # 当前页是否有上一页,有的话返回True,否则返回False
    # print(page1.has_previous())  # False

    # 除了当前页是否有其页,这个也不难理解,比如一页展示10条数据,但总数据也不到10条,那么分页结果就只有一页,它没有其它页了,所以返回False
    # 但这里肯定返回True
    # print(page1.has_other_pages())  # True

    # 返回当前页的开始和结束索引,注意,这个索引不是表中记录的索引,而是分页器为当前分页提供的索引
    # 按照当前示例,每页展示5条记录,我们获取第一页的分页结果,那么起始索引位置就是1,结束位置就是5
    # 可以用来在页面中展示当前记录的序号
    # print(page1.start_index())  # 1
    # print(page1.end_index())  # 5

    # 拿到指定页码的分页对象,可以从这个对象上面获取我们想要的数据
    page20 = pg_obj.page(20)

    # 获取指定页码的记录,返回的是queryset对象
    # print(page20.object_list)  # <QuerySet [<UserInfo: 96-傅莉>, ...., <UserInfo: 99-廖红霞>, <UserInfo: 100-赫梅>]>

    # 获取下一页的页码,这个代码执行肯定报错,因为当前页是最后一页,它没有下一页了
    # print(page20.next_page_number())  # django.core.paginator.EmptyPage: That page contains no results

    # 当前页是否有下一页,有的话返回True,否则返回False
    # print(page20.has_next())  # False

    # 当前页是否有上一页,有的话返回True,否则返回False
    # print(page20.has_previous())  # True

    # 除了当前页是否有其页,这个也不难理解,比如一页展示10条数据,但总数据也不到10条,那么分页结果就只有一页,它没有其它页了,所以返回False
    # 但这里肯定返回True
    # print(page1.has_other_pages())  # True

    # 返回当前页的开始和结束索引,注意,这个索引不是表中记录的索引,而是分页器为当前分页提供的索引
    # 按照当前示例,每页展示5条记录,我们获取的是最后一页的分页结果,那么起始索引位置就是96,结束位置就是100
    # 可以用来在页面中展示当前记录的序号
    # print(page20.start_index())  # 96
    # print(page20.end_index())   # 100

    # 输入的页码不在页码范围内也会报错
    # print(pg_obj.page(0))  # 页码必须大于等于1: django.core.paginator.EmptyPage: That page number is less than 1
    # print(pg_obj.page(1000))  # 页码不在合法的页码范围内: django.core.paginator.EmptyPage: That page contains no results

    # 上面的报错,内部其实用的是validate_number方法,页码合法返回页码值,不合法报错
    # print(pg_obj.validate_number(0))  # django.core.paginator.EmptyPage: That page number is less than 1
    # print(pg_obj.validate_number(5))  # 5
    # print(pg_obj.validate_number(50))  # django.core.paginator.EmptyPage: That page contains no results


if __name__ == '__main__':
    foo()

示例

结合视图和html页面实现一个示例。

python
from django.shortcuts import render
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from apps.mypage.models import UserInfo

def userlist(request):
    paginate_by = 10  # 每页展示的记录条数
    # 查询的queryset数据集,也就是说要对这些查询结果进行分页
    user_queryset = UserInfo.objects.order_by('id')[:202]
    # 获取url中的页码:http://127.0.0.1:8500/page/userlist/?page=4
    current_page = request.GET.get('page')
	# 拿到分页器对象
    pg_obj = Paginator(user_queryset, paginate_by)
    try:
        # 通过分页器对象点page方法传递要获取的页码值,拿到页码对象
        page = pg_obj.page(current_page)
        # URL不传page参数或值不是整型报错:django.core.paginator.PageNotAnInteger: That page number is not an integer
        # URL传page参数,值是0报错:django.core.paginator.EmptyPage: That page number is less than 1
        # URL传page参数,值不在合法页码范围内报错:django.core.paginator.EmptyPage: That page contains no results
    except (PageNotAnInteger, EmptyPage) as e:  # 对于上面的各种情况,统一捕获错误,然后强制返回首页
        # 如果queryset是空的,也不要怕,我们模板语言中可以处理的
        page = pg_obj.page(1)
        # print(page.object_list)  # <QuerySet []>
    return render(request, 'userlist.html', {"page_obj": page})

"""
页面每访问一次,分页器内部就会执行下面两条语句,比如访问:http://127.0.0.1:8500/page/userlist/?page=5
1. 拿到queryset集合的总数量,202条
select count(*) from (select id from mypage_userinfo order by id asc limit 202) subquery;
2. 内部计算出来当前页记录的起始位置和结束位置,再去查数据库。从第40条记录开始往后查10条记录,正好是第五页需要的记录
select * from mypage_userinfo order by id asc limit 10 offset 40;
"""
html
<!DOCTYPE html>
<html lang="en">
<head>
    {% load static %}
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <h1>用户列表</h1>
            <!-- 如果有数据就for循环展示数据 -->
            {% if page_obj.object_list %}
                <table class="table table-bordered table-hover">
                    <thead>
                    <tr>
                        <th>序号</th><th>姓名</th><th>手机号</th><th>邮箱</th>
                    </tr>
                    </thead>
                    <tbody>
                    <!-- 如果有数据就for循环queryset展示数据 -->
                    {% for pg in page_obj.object_list %}
                        <tr>
                            <td>{{ pg.num }}</td><td>{{ pg.username }}</td><td>{{ pg.phone }}</td><td>{{ pg.email }}</td>
                        </tr>
                    {% endfor %}
                    </tbody>
                </table>
            {% else %}
                <div>没有数据</div>
            {% endif %}
            <div>
                <!-- 如果有上一页就展示 -->
                {% if page_obj.has_previous %}
                    <a href="{% url 'mypage:userlist' %}?page=1">首页</a>
                    <a href="{% url 'mypage:userlist' %}?page={{ page_obj.previous_page_number }}">上一页</a>
                {% else %}
                    <!-- 如果没有上一页表示就是首页了,这个按钮就不能点击了 -->
                    <a href="javascript:return false;" style="pointer-events:none;opacity: 0.2">已是首页</a>
                {% endif %}
                <!-- 展示所有页码 -->
                {% for p in page_obj.paginator.page_range %}
                    <a href="{% url 'mypage:userlist' %}?page={{ p }}">{{ p }}</a>
                {% endfor %}
                
                <!-- 如果有下一页就展示 -->
                {% if page_obj.has_next %}
                    <a href="{% url 'mypage:userlist' %}?page={{ page_obj.next_page_number }}">下一页</a>
                    <a href="{% url 'mypage:userlist' %}?page={{ page_obj.paginator.num_pages }}">尾页</a>
                {% else %}
                    <!-- 如果没有下一页表示就表示是最后一页了,这个按钮就不能点击了 -->
                    <a href="javascript:return false;" style="pointer-events:none;opacity: 0.2">已是尾页</a>
                {% endif %}
                <div>
                    展示所有的页码这里,Django提供的原生的分页器只能展示所有的页码,无法实现如下效果:
                    <div>
                        <code>
                            首页 上一页 ...4 5 6 7 8... 下一页 尾页
                        </code>
                    </div>
                    想要这种效果,我们需要对分页器进行二次开发,也就是根据需求自定义分页器
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>
python
from django.db import models


class UserInfo(models.Model):
    num = models.CharField(max_length=32, verbose_name='序号', default=0)
    username = models.CharField(max_length=32, verbose_name='用户名')
    phone = models.CharField(max_length=11, verbose_name='手机号')
    email = models.EmailField(verbose_name='邮箱')

    def __str__(self):
        return f"{self.num}-{self.username}"
python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authapi/', include(('apps.authapi.urls', 'authapi'), namespace='authapi')),
    path('page/', include(('apps.mypage.urls', 'mypage'), namespace='mypage')),
]
python
from django.urls import path
from apps.mypage import views

urlpatterns = [
    path('userlist/', views.userlist, name='userlist'),
]

页面访问效果:

1832669367247044608.png

自定义分页器

主要是结合bootstrap3和自定义分页器实现。

原理就是:

  1. 获取查询集的总数。
  2. 使用自定义分页器生成页码。
  3. 将queryset和分页器对象返回给页面。
  4. 页面中循环queryset展示记录,模板语言渲染分页栏。

主要代码集中在views.py的自定义分页器类PageInfo

python
from django.shortcuts import render
from django.core.paginator import Paginator, InvalidPage, PageNotAnInteger, EmptyPage
from apps.mypage.models import UserInfo


class PageInfo(object):
    """ 自定义分页器类,和内置分页器就没关系了,所有逻辑我们自己实现 """

    def __init__(self, current_page, all_count, per_page, base_url, show_page=11):
        """
        :param current_page:
        :param all_count: 数据库总行数
        :param per_page: 每页显示函数
        :return:
        """
        self.per_page = per_page
        a, b = divmod(all_count, per_page)
        if b:
            a = a + 1
        self.all_pager = a
        self.show_page = show_page
        self.base_url = base_url
        try:
            self.current_page = int(current_page)
            if not (1 <= self.current_page <= self.all_pager):
                self.current_page = 1
        except Exception as e:
            self.current_page = 1
    def start(self):
        return (self.current_page - 1) * self.per_page

    def end(self):
        return self.current_page * self.per_page

    def pager(self):
        page_list = []
        half = int((self.show_page - 1) / 2)
        # 如果数据总页数 < 11
        if self.all_pager < self.show_page:
            begin = 1
            stop = self.all_pager + 1
        # 如果数据总页数 > 11
        else:
            # 如果当前页 <=5,永远显示1,11
            if self.current_page <= half:
                begin = 1
                stop = self.show_page + 1
            else:
                if self.current_page + half > self.all_pager:
                    begin = self.all_pager - self.show_page + 1
                    stop = self.all_pager + 1
                else:
                    begin = self.current_page - half
                    stop = self.current_page + half + 1

        if self.current_page <= 1:
            prev = "<li class='disabled'><a href='#'>上一页</a></li>"
        else:
            prev = "<li><a href='%s?page=%s'>上一页</a></li>" % (self.base_url, self.current_page - 1,)
        page_list.append(prev)

        for i in range(begin, stop):
            if i == self.current_page:
                temp = "<li class='active'><a  href='%s?page=%s'>%s</a></li>" % (self.base_url, i, i,)
            else:
                temp = "<li><a href='%s?page=%s'>%s</a></li>" % (self.base_url, i, i,)
            page_list.append(temp)

        if self.current_page >= self.all_pager:
            nex = "<li class='disabled'><a href='#'>下一页</a></li>"
        else:
            nex = "<li><a href='%s?page=%s'>下一页</a></li>" % (self.base_url, self.current_page + 1,)
        page_list.append(nex)

        # 首页
        first = "<li><a href='%s?page=%s'>首页</a></li>" % (self.base_url, 1)  # 第一页写死一个1就好了
        page_list.insert(0, first)
        # 尾页
        last = "<li><a href='%s?page=%s'>尾页</a></li>" % (self.base_url, self.all_pager)  # 最大页码是self.all_pager
        page_list.append(last)
        return ''.join(page_list)

def userlist(request):
    paginate_by = 10  # 每页展示的记录条数
    current_page = request.GET.get('page')  # 获取url上的页码
    queryset_count = UserInfo.objects.order_by('id').count()
    page_info = PageInfo(current_page, queryset_count, paginate_by, '/page/userlist/', 11)
    user_list = UserInfo.objects.order_by('id')[page_info.start(): page_info.end()]
    return render(request, 'userlist.html', {'user_list': user_list, 'page_info': page_info})
"""
# 每次请求,分页内部执行了两个SQL:
查询第一页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10;
查询1000页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10 offset 9990;
查询最后一页,即第10万页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10 offset 999990;
"""
html
<!DOCTYPE html>
<html lang="en">
<head>
    {% load static %}
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <h1>用户列表</h1>
            <h1>用户列表</h1>
            <!-- 如果有分页对象中数据就for循环展示数据 -->
            {% if user_list %}
                <table class="table table-bordered table-hover">
                    <thead>
                    <tr>
                        <th>序号</th>
                        <th>姓名</th>
                        <th>手机号</th>
                        <th>邮箱</th>
                    </tr>
                    </thead>
                    <tbody>
                    <!-- 如果有数据就for循环queryset展示数据 -->
                    {% for user in user_list %}
                        <tr>
                            <td>{{ user.num }}</td>
                            <td>{{ user.username }}</td>
                            <td>{{ user.phone }}</td>
                            <td>{{ user.email }}</td>
                        </tr>
                    {% endfor %}
                    </tbody>
                </table>
            {% else %}
                <div>
                    <div class="alert alert-warning" role="alert">没有数据!</div>
                </div>
            {% endif %}

            <nav aria-label="Page navigation">
                <ul class="pagination">
                    {{ page_info.pager|safe }}
                </ul>
            </nav>

        </div>
    </div>
</div>
</body>
</html>
python
from django.db import models


class UserInfo(models.Model):
    num = models.CharField(max_length=32, verbose_name='序号', default=0)
    username = models.CharField(max_length=32, verbose_name='用户名')
    phone = models.CharField(max_length=11, verbose_name='手机号')
    email = models.EmailField(verbose_name='邮箱')

    def __str__(self):
        return f"{self.num}-{self.username}"
python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authapi/', include(('apps.authapi.urls', 'authapi'), namespace='authapi')),
    path('page/', include(('apps.mypage.urls', 'mypage'), namespace='mypage')),
]
python
from django.urls import path
from apps.mypage import views

urlpatterns = [
    path('userlist/', views.userlist, name='userlist'),
]

基于内置分页器的二次开发

这部分就是在Django内置的Paginator类的基础上,进行二次开发,主要目的就是实现分页栏的自定义。

代码也集中在视图函数中的MyPaginator;类中了,我为了演示方便,将这个扩展的分页器类放到和视图函数中,你应该把它单独放到一个文件中,方便各个需要的视图函数中引入。

模板渲染数据和分页栏

这里贴出来有改动的代码,其它不变。

python
from django.shortcuts import render
from django.core.paginator import Paginator
from apps.mypage.models import UserInfo


class MyPaginator(Paginator):
    """ 基于内置Paginator类的二次开发 """
    def __init__(
        self,
        queryset,
        request,
        paginate_by=10,
        page_num=11,
        page_prefix='page',
    ):
        # queryset和paginate_by这俩参数交给Paginator实例化时使用的,我们自己扩展分页的其他功能都依赖于父类Paginator,所以先实例化它
        # 注意它的位置应该在所有的实例化代码前面
        super().__init__(queryset, paginate_by)

        # 拿到请求对象request,并重新赋值
        self._request = request

        # 可选参数,决定每页展示多少条数据,默认10条
        self.paginate_by = paginate_by

        # 循环生成的页码数量是11个,原理是当前页码前后再各展示5个页码加上当前页码一共是11个页码,不包含上一页下一页首页尾页这些
        # 当然你也可以指定为其他奇数值,推荐 1~11,这些值,当然你可以自己调整这个值然后观察前端页面看效果
        self.page_num = page_num

        # queryset数据总数据条数除以每页显示的条数,计算出来应该划分多少页

        # 计算出来当前页码左右两边应该展示几个页码按钮,比如一共11个页码按钮,当前页加上左右两边各5个,再加上当前页码一共11个按钮
        self.half = int((self.page_num - 1) // 2)

        # url上拿的分页参数名是page,即/page/userlist/?page=1,你也可以自己指定,比如你想这样携带/page/userlist/?pg=1
        self.page_prefix = page_prefix

        # 页码栏是否展示首页和尾页按钮,默认是展示,你可以指定False不展示这俩按钮
        self.show_fist_last_btn = True

        # 处理从URL上传来的传来的page参数,这个参数的情况可以有多种,但我们这里处理为只要你用这个分页器,那么你只要带的page参数值不合法,
        # 一律返回第一页分页数据,不合法情况包括:没有page参数/page参数值不在合理范围/page参数值是瞎输入任何字符
        try:
            # 只要能拿到page参数,就直接将值转int,如果报错,值就是不合法,直接走except语句赋值为1
            current_page = self._request.GET.get(self.page_prefix)
            self.current_page = int(current_page)
            # 如果值的范围不在合法范围内,一律重新赋值为1,self.num_pages是父类Paginator内部计算出来的最大页码
            if not (1 <= self.current_page <= self.num_pages):
                self.current_page = 1
        except Exception:
            self.current_page = 1

    def gen_pages(self):
        """
        生成页面页码栏中所有页码,返回的是个字符串,字符串内是处理好的分页标签
            如果是Ajax请求,把这个字符串直接放到指定标签中展示就行了
            如果是模板语言,也是直接渲染就好了{{ page_obj.paginator.gen_pages|safe }}
        """
        # 临时存放标签的列表,它的内容长下面这样,如果页面中有不想显示的页码,不要往这个列表中放就好了,也就是把相关代码注释掉就完了
        # page_list = ["首页标签", "上一页标签", "循环生成的标签", "下一页", "尾页标签"]
        page_list = []

        # -------- 展示上一页页码逻辑 --------
        # 如果当前页是1,那么上一页按钮就不能再被点击了,否则正常的处理上一页标签就好了
        if self.current_page <= 1:
            prev_btn = "<li class='disabled'><a href='#'>上一页</a></li>"
        else:
            value = self.deal_with_path_info(self.current_page - 1)
            prev_btn = f"<li><a href='{value}'>上一页</a></li>"
        page_list.append(prev_btn)

        # -------- 循环展示11个页码逻辑 --------
        # 这个self.calculate_pages方法非常重要,因为要显示哪些页码,由btn_start, btn_end值决定,所以要经过复杂的计算
        btn_start, btn_end = self.calculate_pages()

        # 经过一番如此这般之后,得到开始页码和结束页码的值,然后循环生成对应的页码标签机就好了
        for i in range(btn_start, btn_end):
            value = self.deal_with_path_info(i)
            if i == self.current_page:  # 如果页码正好是当前页,就加个选中状态,其余正常的处理标签就好了
                btn = f"<li class='active'><a href='{value}'>{i}</a></li>"
            else:
                btn = f"<li><a href='{value}'>{i}</a></li>"
            page_list.append(btn)

        # -------- 展示下一页页码逻辑 --------
        # 如果当前页是最后一页了,那么下一页按钮就不能再被点击了,否则正常的处理下一页标签就好了
        if self.current_page >= self.num_pages:
            next_btn = "<li class='disabled'><a href='#'>下一页</a></li>"
        else:
            value = self.deal_with_path_info(self.current_page + 1)
            next_btn = f"<li><a href='{value}'>下一页</a></li>"
        page_list.append(next_btn)

        # -------- 首页和尾页码展示 --------
        if self.show_fist_last_btn:
            # 首页
            first_value = self.deal_with_path_info(1)
            first_btn = f"<li><a href='{first_value}'>首页</a></li>"
            page_list.insert(0, first_btn)
            # 尾页
            last_value = self.deal_with_path_info(self.num_pages)
            last_btn = f"<li><a href='{last_value}'>尾页</a></li>"
            page_list.append(last_btn)
        return ''.join(page_list)

    def calculate_pages(self):
        """ 计算起始和结束页码 """
        # 如果数据总页数小于要生成的页码数量,也就是11个,比如说就3页,那么应该循环生成3个翻页按钮就可以了
        if self.num_pages < self.page_num:
            start = 1
            end = self.num_pages + 1
        # 总页数大于最大页码数量,也就是超过11个了,这个分支处理起来也比较复杂了
        else:
            # 如果点击的页码按钮不到最大页码的一半时,固定显示1~11这些页码
            if self.current_page <= self.half:
                start = 1
                end = self.page_num + 1
            # 如果点击的页码按钮开始大于总页码的一半时,取值范围应该是以当前按钮为中间,取左右两边各5个
            else:
                # 这里有极端情况需要处理
                # 如果点击的页码按钮的范围是即将到最后大页码的一半到最大页码这个范围时,那么最右边要显示的页码就是最大页码,然后往左边计算最左边的页码
                # 也就是固定显示最后的11个按钮
                if self.current_page + self.half > self.num_pages:
                    start = self.num_pages - self.page_num + 1
                    end = self.num_pages + 1  # 最大页码
                # 走这个分支就表示分页的数据一定很多,且点击的分页也是中间的数据,不靠近左右两边边界,不需要特殊处理边界值的问题
                # 那就是正常的计算了,取值范围是以当前按钮为中间,取左右两边各5个
                else:
                    start = self.current_page - self.half
                    end = self.current_page + self.half + 1
        print(555,
              f"当前页{self.current_page}, 总页数{self.num_pages}, 页码个数{self.page_num}, start:{start},end:{end}")
        return start, end

    def deal_with_path_info(self, value):
        """
        这一步是非常重要的一步,我们要对请求URL参数的page值进行修改,如果请求参数有额外的参数,还要原封不动的
        给人家加上,最终拼接出来一个可用的URL
        """
        params = self._request.GET.dict()  # {'page': '1', 'k1': 'v1', 'k2': 'v2'}
        params.update({self.page_prefix: value})  # page的值是原来请求URL上的,这个值应该重新赋值为新的值
        params = '&'.join([f"{k}={v}" for k, v in params.items()])  # page=1&k1=v1&k2=v2
        return f"{self._request.path_info}?{params}"  # /page/userlist/?page=1&k1=v1&k2=v2


def userlist(request):
    user_queryset = UserInfo.objects.order_by('id')[:302]
    # 这里只需要将queryset和request对象传递进去,其它都交给分页器内部处理,如果想要自定义其他设置,请参考分页器内部代码
    paginator_obj = MyPaginator(queryset=user_queryset, request=request)  # 使用自定义分页器的默认配置
    page_obj = paginator_obj.page(paginator_obj.current_page)
    return render(request, 'userlist.html', {"page_obj": page_obj})

"""
# 每次请求,分页内部执行了两个SQL:
查询第一页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10;
查询1000页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10 offset 9990;
查询最后一页,即第10万页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10 offset 999990;
"""
html
<!DOCTYPE html>
<html lang="en">
<head>
    {% load static %}
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <h1>用户列表</h1>
            <!-- 如果有分页对象中数据就for循环展示数据 -->
            {% if page_obj.object_list %}
                <table class="table table-bordered table-hover">
                    <thead>
                    <tr>
                        <th>序号</th>
                        <th>姓名</th>
                        <th>手机号</th>
                        <th>邮箱</th>
                    </tr>
                    </thead>
                    <tbody>
                    <!-- 如果有数据就for循环queryset展示数据 -->
                    {% for pg in page_obj.object_list %}
                        <tr>
                            <td>{{ pg.num }}</td>
                            <td>{{ pg.username }}</td>
                            <td>{{ pg.phone }}</td>
                            <td>{{ pg.email }}</td>
                        </tr>
                    {% endfor %}
                    </tbody>
                </table>
            {% else %}
                <div>
                    <div class="alert alert-warning" role="alert">没有数据!</div>
                </div>
            {% endif %}
            <div>
                <h3>这是基于Django提供的分页器Paginator实现的分页栏</h3>
                <!-- 如果有上一页就展示 -->
                {% if page_obj.has_previous %}
                    <a href="{% url 'mypage:userlist' %}?page=1">首页</a>
                    <a href="{% url 'mypage:userlist' %}?page={{ page_obj.previous_page_number }}">上一页</a>
                {% else %}
                    <!-- 如果没有上一页表示就是首页了,这个按钮就不能点击了 -->
                    <a href="javascript:return false;" style="pointer-events:none;opacity: 0.2">已是首页</a>
                {% endif %}
                <!-- 展示所有页码 -->
                {% for p in page_obj.paginator.page_range %}
                    <a href="{% url 'mypage:userlist' %}?page={{ p }}">{{ p }}</a>
                {% endfor %}

                <!-- 如果有下一页就展示 -->
                {% if page_obj.has_next %}
                    <a href="{% url 'mypage:userlist' %}?page={{ page_obj.next_page_number }}">下一页</a>
                    <a href="{% url 'mypage:userlist' %}?page={{ page_obj.paginator.num_pages }}">尾页</a>
                {% else %}
                    <!-- 如果没有下一页表示就表示是最后一页了,这个按钮就不能点击了 -->
                    <a href="javascript:return false;" style="pointer-events:none;opacity: 0.2">已是尾页</a>
                {% endif %}
                <div>
                    展示所有的页码这里,Django提供的原生的分页器只能展示所有的页码,无法实现如下效果:
                    <div>
                        <code>
                            首页 上一页 ...4 5 6 7 8... 下一页 尾页
                        </code>
                    </div>
                    想要这种效果,我们需要对分页器进行二次开发,也就是根据需求自定义分页器
                </div>
            </div>
            <div>
                <h3>这是基于我们自己扩展了Django内置的分页器Paginator实现的分页栏</h3>
                <nav aria-label="Page navigation">
                    <ul class="pagination">
                        {{ page_obj.paginator.gen_pages|safe }}
                    </ul>
                </nav>
            </div>

        </div>
    </div>
</div>
</body>
</html>
python
from django.db import models


class UserInfo(models.Model):
    num = models.CharField(max_length=32, verbose_name='序号', default=0)
    username = models.CharField(max_length=32, verbose_name='用户名')
    phone = models.CharField(max_length=11, verbose_name='手机号')
    email = models.EmailField(verbose_name='邮箱')

    def __str__(self):
        return f"{self.num}-{self.username}"
python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authapi/', include(('apps.authapi.urls', 'authapi'), namespace='authapi')),
    path('page/', include(('apps.mypage.urls', 'mypage'), namespace='mypage')),
]
python
from django.urls import path
from apps.mypage import views

urlpatterns = [
    path('userlist/', views.userlist, name='userlist'),
]

ajax中渲染数据和分页栏

ajax中对分页处理,就需要对原来的我们二次开发分页器类进行一些改造,具体就是:

  • 调整__init__方法,根据请求类型不同,分别从request.GETrequest.POST获取current_page值。
  • 新增gen_ajax_pages方法,这个方法生ajax专用的分页栏的标签。

然后就是前端页面中结合ajax进行调整了。主要代码在views.py和前端页面中。

python
from django.shortcuts import render
from django.http.response import JsonResponse
from django.core.paginator import Paginator
from apps.mypage.models import UserInfo


class MyPaginator(Paginator):
    """ 基于内置Paginator类的二次开发 """

    def __init__(self, queryset, request, paginate_by=10, page_num=11, page_prefix='page', ):
        # queryset和paginate_by这俩参数交给Paginator实例化时使用的,我们自己扩展分页的其他功能都依赖于父类Paginator,所以先实例化它
        # 注意它的位置应该在所有的实例化代码前面
        super().__init__(queryset, paginate_by)

        # 拿到请求对象request,并重新赋值
        self._request = request

        # 可选参数,决定每页展示多少条数据,默认10条
        self.paginate_by = paginate_by

        # 循环生成的页码数量是11个,原理是当前页码前后再各展示5个页码加上当前页码一共是11个页码,不包含上一页下一页首页尾页这些
        # 当然你也可以指定为其他奇数值,推荐 1~11,这些值,当然你可以自己调整这个值然后观察前端页面看效果
        self.page_num = page_num

        # queryset数据总数据条数除以每页显示的条数,计算出来应该划分多少页

        # 计算出来当前页码左右两边应该展示几个页码按钮,比如一共11个页码按钮,当前页加上左右两边各5个,再加上当前页码一共11个按钮
        self.half = int((self.page_num - 1) // 2)

        # url上拿的分页参数名是page,即/page/userlist/?page=1,你也可以自己指定,比如你想这样携带/page/userlist/?pg=1
        self.page_prefix = page_prefix

        # 页码栏是否展示首页和尾页按钮,默认是展示,你可以指定False不展示这俩按钮
        self.show_fist_last_btn = True

        # 处理从URL上传来的传来的page参数,这个参数的情况可以有多种,但我们这里处理为只要你用这个分页器,那么你只要带的page参数值不合法,
        # 一律返回第一页分页数据,不合法情况包括:没有page参数/page参数值不在合理范围/page参数值是瞎输入任何字符
        try:
            # 只要能拿到page参数,就直接将值转int,如果报错,值就是不合法,直接走except语句赋值为1
            if self._request.method == "GET":  # 如果发的是get请求,就从请求参数中拿page值
                current_page = self._request.GET.get(self.page_prefix)
            else:  # 如果是post请求,就从请求体中拿page值,当然了,你也可以一劳永逸都从请求参数中拿page值,这看你怎么想
                current_page = self._request.POST.get(self.page_prefix)
            self.current_page = int(current_page)
            # 如果值的范围不在合法范围内,一律重新赋值为1,self.num_pages是父类Paginator内部计算出来的最大页码
            if not (1 <= self.current_page <= self.num_pages):
                self.current_page = 1
        except Exception:
            self.current_page = 1

    def gen_pages(self):
        """
        在模板语言中用这个方法生成分页按钮,生成页面页码栏中所有页码,返回的是个字符串,字符串内是处理好的分页标签
        模板语言中,直接渲染就好了{{ page_obj.paginator.gen_pages|safe }}
        """
        # 临时存放标签的列表,它的内容长下面这样,如果页面中有不想显示的页码,不要往这个列表中放就好了,也就是把相关代码注释掉就完了
        # page_list = ["首页标签", "上一页标签", "循环生成的标签", "下一页", "尾页标签"]
        page_list = []

        # -------- 展示上一页页码逻辑 --------
        # 如果当前页是1,那么上一页按钮就不能再被点击了,否则正常的处理上一页标签就好了
        if self.current_page <= 1:
            prev_btn = "<li class='disabled'><a href='#'>上一页</a></li>"
        else:
            value = self.deal_with_path_info(self.current_page - 1)
            prev_btn = f"<li><a href='{value}'>上一页</a></li>"
        page_list.append(prev_btn)

        # -------- 循环展示11个页码逻辑 --------
        # 这个self.calculate_pages方法非常重要,因为要显示哪些页码,由btn_start, btn_end值决定,所以要经过复杂的计算
        btn_start, btn_end = self.calculate_pages()

        # 经过一番如此这般之后,得到开始页码和结束页码的值,然后循环生成对应的页码标签机就好了
        for i in range(btn_start, btn_end):
            value = self.deal_with_path_info(i)
            if i == self.current_page:  # 如果页码正好是当前页,就加个选中状态,其余正常的处理标签就好了
                btn = f"<li class='active'><a href='{value}'>{i}</a></li>"
            else:
                btn = f"<li><a href='{value}'>{i}</a></li>"
            page_list.append(btn)

        # -------- 展示下一页页码逻辑 --------
        # 如果当前页是最后一页了,那么下一页按钮就不能再被点击了,否则正常的处理下一页标签就好了
        if self.current_page >= self.num_pages:
            next_btn = "<li class='disabled'><a href='#'>下一页</a></li>"
        else:
            value = self.deal_with_path_info(self.current_page + 1)
            next_btn = f"<li><a href='{value}'>下一页</a></li>"
        page_list.append(next_btn)

        # -------- 首页和尾页码展示 --------
        if self.show_fist_last_btn:
            # 首页
            first_value = self.deal_with_path_info(1)
            first_btn = f"<li><a href='{first_value}'>首页</a></li>"
            page_list.insert(0, first_btn)
            # 尾页
            last_value = self.deal_with_path_info(self.num_pages)
            last_btn = f"<li><a href='{last_value}'>尾页</a></li>"
            page_list.append(last_btn)
        return ''.join(page_list)

    def gen_ajax_pages(self):
        """
        由于Ajax中不需要a标签进行跳转,而是需要Ajax自己获取当前页然后发送到后端,后端分页之后,将处理好的分页按钮和当前页的数据
        封装好,返回给ajax,所以,这里生成页码也需要处理下a标签
        这里的处理逻辑是,a标签禁用点击跳转,转为为li标签绑定自定义属性,值就是具体的页码,前端ajax中绑定事件委派,然后获取这个值往后端发就好了
            <li pageNum='{self.current_page - 1}'><a href='#'>上一页</a></li>
        """
        # 临时存放标签的列表,它的内容长下面这样,如果页面中有不想显示的页码,不要往这个列表中放就好了,也就是把相关代码注释掉就完了
        # page_list = ["首页标签", "上一页标签", "循环生成的标签", "下一页", "尾页标签"]
        page_list = []

        # -------- 展示上一页页码逻辑 --------
        # 如果当前页是1,那么上一页按钮就不能再被点击了,否则正常的处理上一页标签就好了
        if self.current_page <= 1:
            prev_btn = "<li class='disabled'><a href='#'>上一页</a></li>"
        else:
            prev_btn = f"<li pageNum='{self.current_page - 1}'><a href='#'>上一页</a></li>"
        page_list.append(prev_btn)

        # -------- 循环展示11个页码逻辑 --------
        # 这个self.calculate_pages方法非常重要,因为要显示哪些页码,由btn_start, btn_end值决定,所以要经过复杂的计算
        btn_start, btn_end = self.calculate_pages()

        # 经过一番如此这般之后,得到开始页码和结束页码的值,然后循环生成对应的页码标签机就好了
        for i in range(btn_start, btn_end):
            if i == self.current_page:  # 如果页码正好是当前页,就加个选中状态,其余正常的处理标签就好了
                btn = f"<li class='active' pageNum='{i}'><a href='#'>{i}</a></li>"
            else:
                btn = f"<li pageNum='{i}'><a href='#'>{i}</a></li>"
            page_list.append(btn)

        # -------- 展示下一页页码逻辑 --------
        # 如果当前页是最后一页了,那么下一页按钮就不能再被点击了,否则正常的处理下一页标签就好了
        if self.current_page >= self.num_pages:
            next_btn = "<li class='disabled'><a href='#'>下一页</a></li>"
        else:
            next_btn = f"<li pageNum='{self.current_page + 1}'><a href='#'>下一页</a></li>"
        page_list.append(next_btn)

        # -------- 首页和尾页码展示 --------
        if self.show_fist_last_btn:
            # 首页,写死一个1就好了
            first_btn = f"<li pageNum='{1}'><a href='#'>首页</a></li>"
            page_list.insert(0, first_btn)
            # 尾页
            last_btn = f"<li pageNum='{self.num_pages}'><a href='#'>尾页</a></li>"
            page_list.append(last_btn)
        return ''.join(page_list)

    def calculate_pages(self):
        """ 计算起始和结束页码 """
        # 如果数据总页数小于要生成的页码数量,也就是11个,比如说就3页,那么应该循环生成3个翻页按钮就可以了
        if self.num_pages < self.page_num:
            start = 1
            end = self.num_pages + 1
        # 总页数大于最大页码数量,也就是超过11个了,这个分支处理起来也比较复杂了
        else:
            # 如果点击的页码按钮不到最大页码的一半时,固定显示1~11这些页码
            if self.current_page <= self.half:
                start = 1
                end = self.page_num + 1
            # 如果点击的页码按钮开始大于总页码的一半时,取值范围应该是以当前按钮为中间,取左右两边各5个
            else:
                # 这里有极端情况需要处理
                # 如果点击的页码按钮的范围是即将到最后大页码的一半到最大页码这个范围时,那么最右边要显示的页码就是最大页码,然后往左边计算最左边的页码
                # 也就是固定显示最后的11个按钮
                if self.current_page + self.half > self.num_pages:
                    start = self.num_pages - self.page_num + 1
                    end = self.num_pages + 1  # 最大页码
                # 走这个分支就表示分页的数据一定很多,且点击的分页也是中间的数据,不靠近左右两边边界,不需要特殊处理边界值的问题
                # 那就是正常的计算了,取值范围是以当前按钮为中间,取左右两边各5个
                else:
                    start = self.current_page - self.half
                    end = self.current_page + self.half + 1
        print(555,
              f"当前页{self.current_page}, 总页数{self.num_pages}, 页码个数{self.page_num}, start:{start},end:{end}")
        return start, end

    def deal_with_path_info(self, value):
        """
        这一步是非常重要的一步,我们要对请求URL参数的page值进行修改,如果请求参数有额外的参数,还要原封不动的
        给人家加上,最终拼接出来一个可用的URL
        """
        params = self._request.GET.dict()  # {'page': '1', 'k1': 'v1', 'k2': 'v2'}
        params.update({self.page_prefix: value})  # page的值是原来请求URL上的,这个值应该重新赋值为新的值
        params = '&'.join([f"{k}={v}" for k, v in params.items()])  # page=1&k1=v1&k2=v2
        return f"{self._request.path_info}?{params}"  # /page/userlist/?page=1&k1=v1&k2=v2


def userlist(request):
    # 浏览器第一次访问这个视图肯定是get请求,我们直接返回页面,至于页面中的数据和分页内容,都交给ajax处理,get请求分支不处理
    if request.method == "GET":
        return render(request, 'userlist.html')
    # post请求这里,表示从ajax那里获取了查询条件和分页值,我们查询出来数据之后,进行数据和分页处理
    else:
        # 这里你可以根据传来的查询条件进行orm操作,拿到queryset对象
        user_queryset = UserInfo.objects.order_by('id')[:302].values('num', 'username', 'phone', 'email')
        # 将queryset传给分页器进行分页
        paginator_obj = MyPaginator(queryset=user_queryset, request=request)  # 使用自定义分页器的默认配置

        # 分页器对象调用page方法拿到当前页的分页对象
        page_obj = paginator_obj.page(paginator_obj.current_page)

        # 注意,经过分页对象处理后的当前页的数据内容仍然是queryset类型,但我们需要给Ajax返回的是json字符串,所以我们需要先把queryset类型
        # 手动处理为列表,在js中也就是数组,这样通过js代码能够循环展示数据
        user_list = list(page_obj.object_list)
        # 最终只需要将分页栏的最终的字符串和当前页的查询记录返回给Ajax即可,ajax自己往页面渲染就行了
        return JsonResponse({"gen_pages": page_obj.paginator.gen_ajax_pages(), "user_list": user_list})
"""
# 每次请求,分页内部执行了两个SQL:
查询第一页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10;
查询1000页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10 offset 9990;
查询最后一页,即第10万页
	select count(*) from mypage_userinfo;
	select num, username, phone, email from mypage_userinfo order by id asc limit 10 offset 999990;
"""
html
<!DOCTYPE html>
<html lang="en">
<head>
    {% load static %}
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-lg-offset-2">
            <h1>用户列表</h1>
            <div id="noData">
                <table class="table table-bordered table-hover">
                    <thead>
                    <tr>
                        <th>序号</th>
                        <th>姓名</th>
                        <th>手机号</th>
                        <th>邮箱</th>
                    </tr>
                    </thead>
                    <tbody></tbody>
                </table>
            </div>
            <div>
                <h3>这是基于ajax发送post请求实现的翻页</h3>
                <nav aria-label="Page navigation">
                    <!-- 因为生成的标签都是li,所以直接为ul标签绑定一个id=pagination,整个点击事件就完了 -->
                    <ul class="pagination" id="pagination"></ul>
                </nav>
            </div>
        </div>
    </div>
</div>
</body>
<!-- 别忘了引入jQuery -->
<script src="{% static 'js/jquery-1.12.4.min.js' %}"></script>
<script>
    // 思路就是点击分页按钮获取当前li标签中的自定义属性的值,发送到后端,获取当前分页的数据
    $("#pagination").on('click', 'li', function () {
        // console.log($(this).attr("pageNum"))
        let pageNum = $(this).attr("pageNum");
        getData(pageNum)
    })
    // 根据当前页的值向后端发送请求获取数据,然后展示到页面中
    function getData(pageNum = 1) {
        $.ajax({
            // 这里可以从请求参数中提交page值,也可以从请求体中携带page值,这你结合后端来定
            // 我这里选择从请求体中携带page值
            // url: `/page/userlist/${pageNum}`,
            url: `/page/userlist/`,
            method: "POST",
            data: {"page": pageNum, "csrfmiddlewaretoken": "{{ csrf_token }}"},  // csrfmiddlewaretoken参数值是为了处理post请求的跨域问题
            success: function (res) {
                // 展示分页栏
                $("#pagination").html(res.gen_pages)
                // 更新数据
                // 如果有数据就渲染,没数据就给个提示
                if (res.user_list.length === 0) {
                    $("#noData").html(`<div class="alert alert-warning" role="alert">没有数据!</div>`)
                } else {
                    let trs = "";
                    for (let i of res.user_list) {
                        trs += `<tr>
                            <td>${i.num}</td>
                            <td>${i.username}</td>
                            <td>${i.phone}</td>
                            <td>${i.email}</td>
                         </tr>`
                    }
                    $("tbody").html(trs);
                }
            }
        })
    }
    // 页面加载完毕之后,向后端发送一次请求,获取第一页的数据并展示
    // 后续通过点击事件传值获取当前页的数据
    getData()
</script>
</html>
python
from django.db import models


class UserInfo(models.Model):
    num = models.CharField(max_length=32, verbose_name='序号', default=0)
    username = models.CharField(max_length=32, verbose_name='用户名')
    phone = models.CharField(max_length=11, verbose_name='手机号')
    email = models.EmailField(verbose_name='邮箱')

    def __str__(self):
        return f"{self.num}-{self.username}"
python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authapi/', include(('apps.authapi.urls', 'authapi'), namespace='authapi')),
    path('page/', include(('apps.mypage.urls', 'mypage'), namespace='mypage')),
]
python
from django.urls import path
from apps.mypage import views

urlpatterns = [
    path('userlist/', views.userlist, name='userlist'),
]

分页器问题

我们通过学习内置的分页器类了解分页的逻辑。

也通过自定义分页器和分页器的二次开发了解自己搞分页应该如何做,而且,通过我列出的分页的执行SQL,发现无论是自定义分页还是二次开发,都是要查两次:

  • 第一次查询是根据条件或查询结果的总数。
  • 第二次进行分页时,内部通过limit和offset来进行获取当前页的数据。

注意,我通过性能分析,我对一百万条数据进行分页,发现访问前几页SQL的执行时间是20ms~50ms之间,最后几页(访问第10万页)的SQL执行时间是320ms~350ms之间。这也就意味着如果数据量很大的话,越往后翻页越慢!

怎么优化呢,首先能保证的是我们分页查询的SQL语句是走了索引的:

bash
mysql> use dj42
Database changed

-- 这个语句是走了主键索引且是索引覆盖情况,SQL没什么可优化的
mysql> explain select count(*) from mypage_userinfo;
+----+-------------+-----------------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+
| id | select_type | table           | partitions | type  | possible_keys | key     | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+-----------------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | mypage_userinfo | NULL       | index | NULL          | PRIMARY | 8       | NULL | 994885 |   100.00 | Using index |
+----+-------------+-----------------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+
1 row in set, 1 warning (0.00 sec)


-- 这个查询虽然也是走了主键索引,但因为我们查询的记录字段很多,导致没有走索引覆盖,所以,可以考虑优化查询手段,比如只要指定一个或者两个字段,那么为查询的字段添加索引可以提高性能。
-- 另外使用limit和offset分页越往后越慢的原因就是,mysql内部会先扫描offset+n页,然后丢掉前offset调记录,最后返回n调记录,这样的话,就算是走索引也不会快。
mysql> explain select num, username, phone, email from mypage_userinfo order by id asc limit 10 offset 999990;
+----+-------------+-----------------+------------+-------+---------------+---------+---------+------+--------+----------+-------+
| id | select_type | table           | partitions | type  | possible_keys | key     | key_len | ref  | rows   | filtered | Extra |
+----+-------------+-----------------+------------+-------+---------------+---------+---------+------+--------+----------+-------+
|  1 | SIMPLE      | mypage_userinfo | NULL       | index | NULL          | PRIMARY | 8       | NULL | 994885 |   100.00 | NULL  |
+----+-------------+-----------------+------------+-------+---------------+---------+---------+------+--------+----------+-------+
1 row in set, 1 warning (0.00 sec)

除了进行索引优化,其实这个问题也不必太过担心。

因为我们举的例子中,查询结果包含一百万条数据,分页能分十万页,但实际业务中,谁能翻到十万页呢?你可以去看各种网站的分页,其实没那么多页,基本上没等翻页变慢,用户就已经不翻页了。

额外的就是通过游标分页,简单来说就在我们分页查询时,将第2页查询数据作为第1页的查询条件。具体地,查询当前页的上一页、下一页的SQL方式如下所示,这里使用id排序同时也可以保证分页结果的稳定:

bash
-- 查询第N+1页,其中 last_max_id 为第N页时查询结果中最大的ID
select * from mypage_userinfo where id > [last_max_id] order by id asc limit 20;

-- 查询第N-1页,其中 last_min_id 为第N页时查询结果中最小的ID
select * from mypage_userinfo where id < [last_min_id] order by id desc limit 20;

代码示例,在django-restframework框架中,有实现,这不在赘述了。

当然了,游标分页也有其问题,就是只能查看上一页和下一页,不能翻到指定页码,因为你不知道last_max_id或者last_min_id是多少。