Пагинация в Django

Пагинация разбивает большие списки данных на страницы для улучшения производительности и пользовательского опыта. Правильная реализация пагинации критически важна для сайтов с большим количеством контента.

Базовая пагинация во views

  1from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
  2from django.shortcuts import render, get_object_or_404
  3from django.db.models import Q
  4from .models import Book, Author, Category
  5from django.http import JsonResponse
  6import json
  7
  8def book_list(request):
  9    """Список книг с пагинацией"""
 10    # Получаем параметры фильтрации
 11    search_query = request.GET.get('q', '')
 12    category_id = request.GET.get('category', '')
 13    author_id = request.GET.get('author', '')
 14    sort_by = request.GET.get('sort', '-published_date')
 15
 16    # Базовый QuerySet
 17    books = Book.objects.select_related('author').prefetch_related('categories')
 18
 19    # Применяем фильтры
 20    if search_query:
 21        books = books.filter(
 22            Q(title__icontains=search_query) |
 23            Q(description__icontains=search_query) |
 24            Q(author__name__icontains=search_query)
 25        )
 26
 27    if category_id:
 28        books = books.filter(categories__id=category_id)
 29
 30    if author_id:
 31        books = books.filter(author__id=author_id)
 32
 33    # Сортировка
 34    if sort_by == 'price':
 35        books = books.order_by('price')
 36    elif sort_by == '-price':
 37        books = books.order_by('-price')
 38    elif sort_by == 'title':
 39        books = books.order_by('title')
 40    else:
 41        books = books.order_by('-published_date')
 42
 43    # Настройки пагинации
 44    paginator = Paginator(books, 12)  # 12 книг на страницу
 45    page_number = request.GET.get('page')
 46
 47    try:
 48        page_obj = paginator.get_page(page_number)
 49    except PageNotAnInteger:
 50        # Если страница не является числом, показываем первую
 51        page_obj = paginator.page(1)
 52    except EmptyPage:
 53        # Если страница выходит за пределы, показываем последнюю
 54        page_obj = paginator.page(paginator.num_pages)
 55
 56    # Контекст для шаблона
 57    context = {
 58        'page_obj': page_obj,
 59        'books': page_obj.object_list,
 60        'search_query': search_query,
 61        'category_id': category_id,
 62        'author_id': author_id,
 63        'sort_by': sort_by,
 64        'categories': Category.objects.all(),
 65        'authors': Author.objects.all(),
 66        'total_books': paginator.count,
 67        'total_pages': paginator.num_pages,
 68    }
 69
 70    return render(request, 'books/book_list.html', context)
 71
 72# Class-based view с пагинацией
 73from django.views.generic import ListView
 74from django.core.paginator import Paginator
 75
 76class BookListView(ListView):
 77    model = Book
 78    template_name = 'books/book_list.html'
 79    context_object_name = 'books'
 80    paginate_by = 12
 81    ordering = ['-published_date']
 82
 83    def get_queryset(self):
 84        """Кастомизация QuerySet с фильтрацией"""
 85        queryset = Book.objects.select_related('author').prefetch_related('categories')
 86
 87        # Поиск
 88        search_query = self.request.GET.get('q')
 89        if search_query:
 90            queryset = queryset.filter(
 91                Q(title__icontains=search_query) |
 92                Q(description__icontains=search_query)
 93            )
 94
 95        # Фильтрация по категории
 96        category_id = self.request.GET.get('category')
 97        if category_id:
 98            queryset = queryset.filter(categories__id=category_id)
 99
100        return queryset
101
102    def get_context_data(self, **kwargs):
103        """Дополнительный контекст"""
104        context = super().get_context_data(**kwargs)
105        context.update({
106            'search_query': self.request.GET.get('q', ''),
107            'category_id': self.request.GET.get('category', ''),
108            'categories': Category.objects.all(),
109            'total_books': self.get_queryset().count(),
110        })
111        return context

Кастомные пагинаторы

Пагинатор с диапазоном страниц

 1class CustomPaginator(Paginator):
 2    """Кастомный пагинатор с диапазоном страниц"""
 3
 4    def get_page_range(self, current_page, window=2):
 5        """Возвращает диапазон страниц для отображения"""
 6        if self.num_pages <= 2 * window + 1:
 7            # Если страниц мало, показываем все
 8            return range(1, self.num_pages + 1)
 9
10        # Вычисляем границы диапазона
11        start = max(1, current_page - window)
12        end = min(self.num_pages, current_page + window)
13
14        # Добавляем первую и последнюю страницу
15        page_range = []
16        if start > 1:
17            page_range.extend([1, '...'])
18
19        page_range.extend(range(start, end + 1))
20
21        if end < self.num_pages:
22            page_range.extend(['...', self.num_pages])
23
24        return page_range
25
26# Использование кастомного пагинатора
27def book_list_with_custom_pagination(request):
28    books = Book.objects.all()
29    paginator = CustomPaginator(books, 10)
30    page_number = request.GET.get('page', 1)
31
32    try:
33        page_obj = paginator.get_page(page_number)
34        page_range = paginator.get_page_range(page_obj.number)
35    except (PageNotAnInteger, EmptyPage):
36        page_obj = paginator.page(1)
37        page_range = paginator.get_page_range(1)
38
39    context = {
40        'page_obj': page_obj,
41        'page_range': page_range,
42        'books': page_obj.object_list,
43    }
44
45    return render(request, 'books/book_list.html', context)
46
47# Пагинатор с информацией о соседних страницах
48class SmartPaginator(Paginator):
49    """Умный пагинатор с информацией о соседних страницах"""
50
51    def get_page_info(self, current_page):
52        """Возвращает информацию о текущей странице и соседних"""
53        try:
54            page_obj = self.page(current_page)
55        except (PageNotAnInteger, EmptyPage):
56            page_obj = self.page(1)
57
58        # Информация о соседних страницах
59        has_previous = page_obj.has_previous()
60        has_next = page_obj.has_next()
61
62        if has_previous:
63            previous_page_number = page_obj.previous_page_number()
64        else:
65            previous_page_number = None
66
67        if has_next:
68            next_page_number = page_obj.next_page_number()
69        else:
70            next_page_number = None
71
72        # Диапазон страниц
73        page_range = self.get_page_range(page_obj.number)
74
75        return {
76            'page_obj': page_obj,
77            'has_previous': has_previous,
78            'has_next': has_next,
79            'previous_page_number': previous_page_number,
80            'next_page_number': next_page_number,
81            'page_range': page_range,
82            'total_pages': self.num_pages,
83            'total_count': self.count,
84            'current_page': page_obj.number,
85        }

Пагинатор для бесконечной прокрутки

 1class InfiniteScrollPaginator(Paginator):
 2    """Пагинатор для бесконечной прокрутки"""
 3
 4    def get_page_data(self, page_number, items_per_page=20):
 5        """Возвращает данные для конкретной страницы"""
 6        start = (page_number - 1) * items_per_page
 7        end = start + items_per_page
 8
 9        return {
10            'items': self.object_list[start:end],
11            'has_next': end < self.count,
12            'next_page': page_number + 1 if end < self.count else None,
13            'total_count': self.count,
14            'current_page': page_number,
15        }
16
17# View для бесконечной прокрутки
18def infinite_scroll_books(request):
19    """API для бесконечной прокрутки книг"""
20    if request.method == 'GET':
21        page_number = int(request.GET.get('page', 1))
22        items_per_page = int(request.GET.get('per_page', 20))
23
24        books = Book.objects.select_related('author').prefetch_related('categories')
25
26        # Применяем фильтры
27        category_id = request.GET.get('category')
28        if category_id:
29            books = books.filter(categories__id=category_id)
30
31        author_id = request.GET.get('author')
32        if author_id:
33            books = books.filter(author__id=author_id)
34
35        # Сортировка
36        sort_by = request.GET.get('sort', '-published_date')
37        if sort_by == 'price':
38            books = books.order_by('price')
39        elif sort_by == '-price':
40            books = books.order_by('-price')
41        else:
42            books = books.order_by('-published_date')
43
44        paginator = InfiniteScrollPaginator(books, items_per_page)
45
46        try:
47            page_data = paginator.get_page_data(page_number, items_per_page)
48
49            # Сериализуем данные
50            books_data = []
51            for book in page_data['items']:
52                books_data.append({
53                    'id': book.id,
54                    'title': book.title,
55                    'author': book.author.name,
56                    'price': str(book.price),
57                    'cover_url': book.cover_image.url if book.cover_image else None,
58                    'categories': [cat.name for cat in book.categories.all()],
59                    'rating': book.get_average_rating(),
60                    'review_count': book.reviews.count(),
61                })
62
63            return JsonResponse({
64                'success': True,
65                'books': books_data,
66                'has_next': page_data['has_next'],
67                'next_page': page_data['next_page'],
68                'total_count': page_data['total_count'],
69                'current_page': page_data['current_page'],
70            })
71
72        except Exception as e:
73            return JsonResponse({
74                'success': False,
75                'error': str(e)
76            }, status=500)
77
78    return JsonResponse({'error': 'Method not allowed'}, status=405)

Шаблоны для пагинации

Базовый шаблон пагинации

 1<!-- templates/includes/pagination.html -->
 2{% if page_obj.has_other_pages %}
 3<nav aria-label="Навигация по страницам">
 4    <ul class="pagination justify-content-center">
 5        <!-- Кнопка "Предыдущая" -->
 6        {% if page_obj.has_previous %}
 7            <li class="page-item">
 8                <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.previous_page_number }}" aria-label="Предыдущая">
 9                    <span aria-hidden="true">&laquo;</span>
10                </a>
11            </li>
12        {% else %}
13            <li class="page-item disabled">
14                <span class="page-link" aria-label="Предыдущая">
15                    <span aria-hidden="true">&laquo;</span>
16                </span>
17            </li>
18        {% endif %}
19
20        <!-- Номера страниц -->
21        {% for page_num in page_obj.paginator.page_range %}
22            {% if page_num == page_obj.number %}
23                <li class="page-item active">
24                    <span class="page-link">{{ page_num }}</span>
25                </li>
26            {% elif page_num > page_obj.number|add:'-3' and page_num < page_obj.number|add:'3' %}
27                <li class="page-item">
28                    <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_num }}">{{ page_num }}</a>
29                </li>
30            {% endif %}
31        {% endfor %}
32
33        <!-- Кнопка "Следующая" -->
34        {% if page_obj.has_next %}
35            <li class="page-item">
36                <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.next_page_number }}" aria-label="Следующая">
37                    <span aria-hidden="true">&raquo;</span>
38                </a>
39            </li>
40        {% else %}
41            <li class="page-item disabled">
42                <span class="page-link" aria-label="Следующая">
43                    <span aria-hidden="true">&raquo;</span>
44                </span>
45            </li>
46        {% endif %}
47    </ul>
48</nav>
49
50<!-- Информация о страницах -->
51<div class="text-center text-muted">
52    <small>
53        Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}
54        (Всего {{ page_obj.paginator.count }} элементов)
55    </small>
56</div>
57{% endif %}

Кастомный шаблон с диапазоном страниц

 1<!-- templates/includes/smart_pagination.html -->
 2{% if page_obj.has_other_pages %}
 3<nav aria-label="Навигация по страницам">
 4    <ul class="pagination pagination-lg justify-content-center">
 5        <!-- Первая страница -->
 6        {% if page_obj.number > 1 %}
 7            <li class="page-item">
 8                <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page=1" aria-label="Первая страница">
 9                    <i class="fas fa-angle-double-left"></i>
10                </a>
11            </li>
12        {% endif %}
13
14        <!-- Предыдущая страница -->
15        {% if page_obj.has_previous %}
16            <li class="page-item">
17                <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.previous_page_number }}" aria-label="Предыдущая страница">
18                    <i class="fas fa-angle-left"></i>
19                </a>
20            </li>
21        {% endif %}
22
23        <!-- Диапазон страниц -->
24        {% for page_item in page_range %}
25            {% if page_item == '...' %}
26                <li class="page-item disabled">
27                    <span class="page-link">{{ page_item }}</span>
28                </li>
29            {% elif page_item == page_obj.number %}
30                <li class="page-item active">
31                    <span class="page-link">{{ page_item }}</span>
32                </li>
33            {% else %}
34                <li class="page-item">
35                    <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_item }}">{{ page_item }}</a>
36                </li>
37            {% endif %}
38        {% endfor %}
39
40        <!-- Следующая страница -->
41        {% if page_obj.has_next %}
42            <li class="page-item">
43                <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.next_page_number }}" aria-label="Следующая страница">
44                    <i class="fas fa-angle-right"></i>
45                </a>
46            </li>
47        {% endif %}
48
49        <!-- Последняя страница -->
50        {% if page_obj.number < page_obj.paginator.num_pages %}
51            <li class="page-item">
52                <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.paginator.num_pages }}" aria-label="Последняя страница">
53                    <i class="fas fa-angle-double-right"></i>
54                </a>
55            </li>
56        {% endif %}
57    </ul>
58</nav>
59
60<!-- Дополнительная информация -->
61<div class="row">
62    <div class="col-md-6">
63        <div class="d-flex justify-content-start">
64            <div class="btn-group" role="group">
65                <button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
66                    {{ page_obj.paginator.per_page }} на странице
67                </button>
68                <ul class="dropdown-menu">
69                    <li><a class="dropdown-item" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}per_page=10">10 на странице</a></li>
70                    <li><a class="dropdown-item" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}per_page=20">20 на странице</a></li>
71                    <li><a class="dropdown-item" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}per_page=50">50 на странице</a></li>
72                </ul>
73            </div>
74        </div>
75    </div>
76    <div class="col-md-6">
77        <div class="d-flex justify-content-end">
78            <small class="text-muted">
79                Показано {{ page_obj.start_index }}-{{ page_obj.end_index }}
80                из {{ page_obj.paginator.count }} элементов
81            </small>
82        </div>
83    </div>
84</div>
85{% endif %}

AJAX пагинация

JavaScript для AJAX пагинации

  1// static/js/ajax_pagination.js
  2class AjaxPagination {
  3    constructor(container, options = {}) {
  4        this.container = container;
  5        this.options = {
  6            url: options.url || window.location.pathname,
  7            pageParam: options.pageParam || 'page',
  8            perPageParam: options.perPageParam || 'per_page',
  9            perPage: options.perPage || 20,
 10            onPageLoad: options.onPageLoad || null,
 11            ...options
 12        };
 13
 14        this.currentPage = 1;
 15        this.isLoading = false;
 16
 17        this.init();
 18    }
 19
 20    init() {
 21        // Обработчик клика по пагинации
 22        this.container.addEventListener('click', (e) => {
 23            if (e.target.classList.contains('page-link') && !e.target.parentElement.classList.contains('disabled')) {
 24                e.preventDefault();
 25                const page = e.target.dataset.page || e.target.getAttribute('href').match(/page=(\d+)/)?.[1];
 26                if (page) {
 27                    this.loadPage(parseInt(page));
 28                }
 29            }
 30        });
 31
 32        // Обработчик изменения количества элементов на странице
 33        const perPageSelect = this.container.querySelector('.per-page-select');
 34        if (perPageSelect) {
 35            perPageSelect.addEventListener('change', (e) => {
 36                this.options.perPage = parseInt(e.target.value);
 37                this.loadPage(1);
 38            });
 39        }
 40    }
 41
 42    async loadPage(page) {
 43        if (this.isLoading) return;
 44
 45        this.isLoading = true;
 46        this.showLoading();
 47
 48        try {
 49            const url = new URL(this.options.url, window.location.origin);
 50            url.searchParams.set(this.options.pageParam, page);
 51            url.searchParams.set(this.options.perPageParam, this.options.perPage);
 52
 53            // Добавляем текущие GET параметры
 54            const currentParams = new URLSearchParams(window.location.search);
 55            for (const [key, value] of currentParams) {
 56                if (key !== this.options.pageParam && key !== this.options.perPageParam) {
 57                    url.searchParams.set(key, value);
 58                }
 59            }
 60
 61            const response = await fetch(url, {
 62                headers: {
 63                    'X-Requested-With': 'XMLHttpRequest'
 64                }
 65            });
 66
 67            if (!response.ok) {
 68                throw new Error(`HTTP error! status: ${response.status}`);
 69            }
 70
 71            const data = await response.json();
 72
 73            if (data.success) {
 74                this.updateContent(data);
 75                this.currentPage = page;
 76                this.updateURL(page);
 77
 78                if (this.options.onPageLoad) {
 79                    this.options.onPageLoad(data, page);
 80                }
 81            } else {
 82                throw new Error(data.error || 'Unknown error');
 83            }
 84
 85        } catch (error) {
 86            console.error('Error loading page:', error);
 87            this.showError(error.message);
 88        } finally {
 89            this.isLoading = false;
 90            this.hideLoading();
 91        }
 92    }
 93
 94    updateContent(data) {
 95        // Обновляем содержимое
 96        const contentContainer = document.querySelector(this.options.contentSelector || '.content-container');
 97        if (contentContainer && data.html) {
 98            contentContainer.innerHTML = data.html;
 99        }
100
101        // Обновляем пагинацию
102        const paginationContainer = document.querySelector(this.options.paginationSelector || '.pagination-container');
103        if (paginationContainer && data.pagination) {
104            paginationContainer.innerHTML = data.pagination;
105        }
106
107        // Обновляем информацию о страницах
108        const infoContainer = document.querySelector(this.options.infoSelector || '.pagination-info');
109        if (infoContainer && data.info) {
110            infoContainer.innerHTML = data.info;
111        }
112    }
113
114    updateURL(page) {
115        const url = new URL(window.location);
116        url.searchParams.set(this.options.pageParam, page);
117
118        // Обновляем URL без перезагрузки страницы
119        window.history.pushState({}, '', url);
120    }
121
122    showLoading() {
123        const loadingEl = this.container.querySelector('.loading');
124        if (loadingEl) {
125            loadingEl.style.display = 'block';
126        }
127    }
128
129    hideLoading() {
130        const loadingEl = this.container.querySelector('.loading');
131        if (loadingEl) {
132            loadingEl.style.display = 'none';
133        }
134    }
135
136    showError(message) {
137        const errorEl = this.container.querySelector('.error-message');
138        if (errorEl) {
139            errorEl.textContent = message;
140            errorEl.style.display = 'block';
141        }
142    }
143}
144
145// Инициализация AJAX пагинации
146document.addEventListener('DOMContentLoaded', function() {
147    const paginationContainer = document.querySelector('.pagination-container');
148    if (paginationContainer) {
149        const pagination = new AjaxPagination(paginationContainer, {
150            url: '/books/',
151            perPage: 20,
152            contentSelector: '.books-grid',
153            paginationSelector: '.pagination-container',
154            infoSelector: '.pagination-info',
155            onPageLoad: function(data, page) {
156                // Дополнительные действия при загрузке страницы
157                console.log(`Страница ${page} загружена`);
158
159                // Обновляем счетчики
160                if (data.total_count) {
161                    document.querySelector('.total-books').textContent = data.total_count;
162                }
163            }
164        });
165    }
166});

View для AJAX пагинации

 1from django.template.loader import render_to_string
 2from django.http import JsonResponse
 3
 4def ajax_book_list(request):
 5    """AJAX view для списка книг с пагинацией"""
 6    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
 7        # Получаем параметры
 8        page = int(request.GET.get('page', 1))
 9        per_page = int(request.GET.get('per_page', 20))
10
11        # Фильтрация и сортировка
12        books = Book.objects.select_related('author').prefetch_related('categories')
13
14        search_query = request.GET.get('q')
15        if search_query:
16            books = books.filter(
17                Q(title__icontains=search_query) |
18                Q(description__icontains=search_query)
19            )
20
21        category_id = request.GET.get('category')
22        if category_id:
23            books = books.filter(categories__id=category_id)
24
25        # Сортировка
26        sort_by = request.GET.get('sort', '-published_date')
27        if sort_by == 'price':
28            books = books.order_by('price')
29        elif sort_by == '-price':
30            books = books.order_by('-price')
31        else:
32            books = books.order_by('-published_date')
33
34        # Пагинация
35        paginator = Paginator(books, per_page)
36
37        try:
38            page_obj = paginator.get_page(page)
39        except (PageNotAnInteger, EmptyPage):
40            page_obj = paginator.page(1)
41
42        # Рендерим HTML для содержимого
43        content_html = render_to_string('books/includes/book_grid.html', {
44            'books': page_obj.object_list,
45            'request': request
46        })
47
48        # Рендерим HTML для пагинации
49        pagination_html = render_to_string('includes/smart_pagination.html', {
50            'page_obj': page_obj,
51            'request': request
52        })
53
54        # Рендерим HTML для информации
55        info_html = render_to_string('books/includes/pagination_info.html', {
56            'page_obj': page_obj,
57            'request': request
58        })
59
60        return JsonResponse({
61            'success': True,
62            'html': content_html,
63            'pagination': pagination_html,
64            'info': info_html,
65            'current_page': page_obj.number,
66            'total_pages': paginator.num_pages,
67            'total_count': paginator.count,
68            'has_next': page_obj.has_next(),
69            'has_previous': page_obj.has_previous(),
70        })
71
72    # Обычный GET запрос - показываем полную страницу
73    return book_list(request)

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

Кэширование пагинированных результатов

 1from django.core.cache import cache
 2from django.utils.decorators import method_decorator
 3from django.views.decorators.cache import cache_page
 4
 5class CachedBookListView(ListView):
 6    model = Book
 7    template_name = 'books/book_list.html'
 8    context_object_name = 'books'
 9    paginate_by = 20
10
11    def get_cache_key(self, page_number):
12        """Генерирует ключ кэша для страницы"""
13        # Создаем уникальный ключ на основе параметров
14        cache_params = {
15            'page': page_number,
16            'per_page': self.paginate_by,
17            'search': self.request.GET.get('q', ''),
18            'category': self.request.GET.get('category', ''),
19            'sort': self.request.GET.get('sort', ''),
20        }
21
22        # Сортируем параметры для консистентности
23        sorted_params = sorted(cache_params.items())
24        cache_key = f"book_list:{hash(str(sorted_params))}"
25
26        return cache_key
27
28    def get_queryset(self):
29        """Получает QuerySet с кэшированием"""
30        cache_key = self.get_cache_key(1)  # Кэшируем первую страницу
31
32        # Пытаемся получить из кэша
33        cached_queryset = cache.get(cache_key)
34        if cached_queryset is not None:
35            return cached_queryset
36
37        # Если нет в кэше, создаем и кэшируем
38        queryset = Book.objects.select_related('author').prefetch_related('categories')
39
40        # Применяем фильтры
41        search_query = self.request.GET.get('q')
42        if search_query:
43            queryset = queryset.filter(
44                Q(title__icontains=search_query) |
45                Q(description__icontains=search_query)
46            )
47
48        category_id = self.request.GET.get('category')
49        if category_id:
50            queryset = queryset.filter(categories__id=category_id)
51
52        # Сортировка
53        sort_by = self.request.GET.get('sort', '-published_date')
54        if sort_by == 'price':
55            queryset = queryset.order_by('price')
56        elif sort_by == '-price':
57            queryset = queryset.order_by('-price')
58        else:
59            queryset = queryset.order_by('-published_date')
60
61        # Кэшируем на 5 минут
62        cache.set(cache_key, queryset, 300)
63
64        return queryset
65
66# Кэширование отдельных страниц
67@method_decorator(cache_page(60 * 15))  # 15 минут
68def cached_book_list(request):
69    """Кэшированная версия списка книг"""
70    return book_list(request)

Ленивая загрузка для больших списков

 1from django.core.paginator import Paginator
 2from django.db.models import Prefetch
 3
 4class LazyBookListView(ListView):
 5    model = Book
 6    template_name = 'books/book_list_lazy.html'
 7    context_object_name = 'books'
 8    paginate_by = 50  # Больше элементов на страницу для ленивой загрузки
 9
10    def get_queryset(self):
11        """Оптимизированный QuerySet с ленивой загрузкой"""
12        queryset = Book.objects.select_related('author').prefetch_related(
13            Prefetch(
14                'categories',
15                queryset=Category.objects.only('name', 'slug')
16            ),
17            Prefetch(
18                'reviews',
19                queryset=Review.objects.only('rating').select_related('user')
20            )
21        )
22
23        # Применяем фильтры
24        search_query = self.request.GET.get('q')
25        if search_query:
26            queryset = queryset.filter(
27                Q(title__icontains=search_query) |
28                Q(description__icontains=search_query)
29            )
30
31        return queryset
32
33    def get_context_data(self, **kwargs):
34        """Дополнительный контекст для ленивой загрузки"""
35        context = super().get_context_data(**kwargs)
36
37        # Добавляем информацию для ленивой загрузки
38        context['lazy_loading'] = True
39        context['load_more_url'] = reverse('books:load_more')
40
41        return context
42
43# API для ленивой загрузки
44def load_more_books(request):
45    """API для загрузки дополнительных книг"""
46    if request.method == 'GET':
47        offset = int(request.GET.get('offset', 0))
48        limit = int(request.GET.get('limit', 20))
49
50        books = Book.objects.select_related('author').prefetch_related('categories')[offset:offset + limit]
51
52        # Сериализуем данные
53        books_data = []
54        for book in books:
55            books_data.append({
56                'id': book.id,
57                'title': book.title,
58                'author': book.author.name,
59                'price': str(book.price),
60                'cover_url': book.cover_image.url if book.cover_image else None,
61                'categories': [cat.name for cat in book.categories.all()],
62            })
63
64        return JsonResponse({
65            'success': True,
66            'books': books_data,
67            'has_more': len(books_data) == limit,
68            'next_offset': offset + limit,
69        })
70
71    return JsonResponse({'error': 'Method not allowed'}, status=405)

Лучшие практики

  • Используй select_related и prefetch_related - для оптимизации запросов
  • Кэшируй пагинированные результаты - особенно для часто запрашиваемых страниц
  • Ограничивай количество элементов на странице - оптимально 20-50 элементов
  • Сохраняй GET параметры при пагинации - используй request.GET.urlencode
  • Добавляй информацию о страницах - показывай текущую страницу и общее количество
  • Используй AJAX для динамической загрузки - для лучшего UX
  • Оптимизируй изображения - используй lazy loading для изображений
  • Добавляй индикаторы загрузки - показывай состояние загрузки страниц

Частые ошибки и их решения

Ошибка: "Page not an integer"

1# Проблема: неправильная обработка номера страницы
2# Решение: используй try-except блок
3
4try:
5    page_obj = paginator.get_page(page_number)
6except PageNotAnInteger:
7    page_obj = paginator.page(1)
8except EmptyPage:
9    page_obj = paginator.page(paginator.num_pages)

Ошибка: "N+1 queries" при пагинации

1# Проблема: множественные запросы к связанным объектам
2# Решение: используй select_related и prefetch_related
3
4books = Book.objects.select_related('author').prefetch_related('categories')
5paginator = Paginator(books, 20)

FAQ

Q: Как сохранить GET параметры при пагинации?
A: Используй request.GET.urlencode в ссылках пагинации или создай кастомный template tag.

Q: Можно ли создать бесконечную прокрутку?
A: Да, используй AJAX для загрузки дополнительных данных при прокрутке до конца страницы.

Q: Как кэшировать пагинированные результаты?
A: Используй Django cache framework и создавай уникальные ключи кэша для каждой страницы.

Q: Можно ли изменить количество элементов на странице?
A: Да, добавь параметр per_page в GET запрос и передавай его в Paginator.

Q: Как добавить сортировку с пагинацией?
A: Добавь параметр sort в GET запрос и применяй ordering к QuerySet перед пагинацией.

Q: Можно ли создать кастомный пагинатор?
A: Да, наследуйся от Paginator и переопредели нужные методы для кастомной логики.