← Назад к вопросам

Как устроена пагинация в Django?

2.0 Middle🔥 231 комментариев
#Django#REST API и HTTP

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Пагинация в Django: полное устройство

Пагинация в Django — это механизм разделения больших наборов данных на страницы для удобного просмотра. Рассмотрим как это работает под капотом и как использовать эффективно.

Основные компоненты пагинации

Django предоставляет класс Paginator из модуля django.core.paginator, который управляет разбиением queryset на страницы.

from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Article

def article_list(request):
    articles = Article.objects.all()  # QuerySet со всеми статьями
    paginator = Paginator(articles, 10)  # 10 статей на странице
    
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)  # Получаем объект страницы
    
    return render(request, "articles/list.html", {"page_obj": page_obj})

Как работает Paginator внутри

Класс Paginator делает следующее:

  1. Вычисляет количество страниц: Берет общее количество объектов и делит на количество элементов на странице
  2. Проверяет номер страницы: Убеждается, что номер находится в допустимом диапазоне
  3. Вычисляет offset и limit: Для каждой страницы вычисляет, какие строки база должна вернуть
paginator = Paginator(queryset, per_page=10)
# Paginator вычисляет:
# - count: общее количество объектов
# - num_pages: количество страниц
# - page_range: список номеров страниц [1, 2, 3, ...]

QuerySet оптимизация

Важно понимать, что пагинация не загружает все данные в памяти сразу. QuerySet ленивый:

queryse = Article.objects.all()  # SQL еще не выполнен
paginator = Paginator(queryset, 10)
page_obj = paginator.get_page(1)  # SQL выполнен с LIMIT и OFFSET

# Эквивалентный SQL для первой страницы:
# SELECT * FROM articles LIMIT 10 OFFSET 0

Полный пример с template

# views.py
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .models import Article

def article_list(request):
    articles = Article.objects.all().order_by("-published_date")
    paginator = Paginator(articles, 15)
    
    page_number = request.GET.get("page", 1)
    
    try:
        page_obj = paginator.page(page_number)
    except PageNotAnInteger:
        page_obj = paginator.page(1)  # Первая страница при некорректном вводе
    except EmptyPage:
        page_obj = paginator.page(paginator.num_pages)  # Последняя страница при выходе за границы
    
    context = {
        "page_obj": page_obj,
        "articles": page_obj.object_list,  # Статьи текущей страницы
    }
    return render(request, "articles/list.html", context)

Методы объекта Page

Объект страницы page_obj предоставляет полезные методы и свойства:

page_obj.number           # Номер текущей страницы (1, 2, 3, ...)
page_obj.object_list      # QuerySet с объектами текущей страницы
page_obj.paginator        # Ссылка на Paginator
page_obj.has_next()       # Есть ли следующая страница
page_obj.has_previous()   # Есть ли предыдущая страница
page_obj.next_page_number()      # Номер следующей страницы
page_obj.previous_page_number()  # Номер предыдущей страницы
page_obj.start_index()    # Индекс первого элемента на странице (глобальный)
page_obj.end_index()      # Индекс последнего элемента на странице (глобальный)

Использование в template

<!-- articles/list.html -->
<div class="articles">
    {% for article in page_obj %}
        <div class="article">
            <h2>{{ article.title }}</h2>
            <p>{{ article.content }}</p>
        </div>
    {% endfor %}
</div>

<!-- Навигация по страницам -->
<div class="pagination">
    {% if page_obj.has_previous %}
        <a href="?page=1">« Первая</a>
        <a href="?page={{ page_obj.previous_page_number }}">< Назад</a>
    {% endif %}
    
    <span class="current">
        Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}
    </span>
    
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">Вперёд ></a>
        <a href="?page={{ page_obj.paginator.num_pages }}">Последняя »</a>
    {% endif %}
</div>

<!-- Опционально: навигация со всеми номерами страниц -->
<div class="page-numbers">
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <span class="current">{{ num }}</span>
        {% else %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
</div>

Оптимизация пагинации

Для больших наборов данных есть несколько стратегий оптимизации:

1. Ограничение количества выводимых страниц

# Вместо всех страниц, показываем только близлежащие
page_range = paginator.page_range
if page_obj.number > 5:
    page_range = list(range(page_obj.number - 2, page_obj.number + 3))

2. Keyset pagination (cursor-based)

Для очень больших таблиц используют cursor-based пагинацию вместо offset:

from django.db.models import Q

def get_next_articles(last_id=None, per_page=10):
    queryset = Article.objects.order_by("id")
    
    if last_id:
        queryset = queryset.filter(id__gt=last_id)
    
    return queryset[:per_page+1]  # +1 для проверки есть ли еще

3. Кэширование count()

from django.core.cache import cache

def get_article_count():
    count = cache.get("article_count")
    if count is None:
        count = Article.objects.count()
        cache.set("article_count", count, 3600)  # На час
    return count

REST API пагинация

В DRF (Django REST Framework) пагинация настраивается в settings:

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20
}

# Или cursor-based для очень больших наборов:
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.CursorPagination",
"PAGE_SIZE": 20

Частые ошибки

1. Использование offset для очень больших страниц — медленно, используй cursor-based

2. Не кэшировать count() — при каждом запросе пересчитывается

3. Пагинировать неотсортированный queryset — результаты непредсказуемы

4. Забыть обработать исключения — используй try/except с PageNotAnInteger и EmptyPage

Пагинация в Django — это мощный инструмент, который при правильном использовании обеспечивает эффективную работу с большими наборами данных.