Как устроена пагинация в Django?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Пагинация в 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 делает следующее:
- Вычисляет количество страниц: Берет общее количество объектов и делит на количество элементов на странице
- Проверяет номер страницы: Убеждается, что номер находится в допустимом диапазоне
- Вычисляет 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 — это мощный инструмент, который при правильном использовании обеспечивает эффективную работу с большими наборами данных.