📖 Теория — Урок 31

← К оглавлению урока

⚡ Ключевые концепции урока 31

  • Extract* (Django ORM): функции для извлечения компонентов даты — год, месяц, квартал, день, день недели, час, минута, секунда, номер недели
  • Прямая фильтрация: filter(date__year=2023) — без явного импорта функций
  • Аннотация + Extract: annotate(year=ExtractYear('date')).filter(year=2023) — мощнее, поддерживает агрегации
  • request.query_params: словарь GET-параметров в DRF APIView
  • Фильтрация: передаём параметры в filter(**filters) только при наличии значений
  • Сортировка: sort_by + sort_order через order_by(), префикс - для убывания
  • Пагинация: PageNumberPagination, paginate_queryset(), get_paginated_response()

Часть 1: Функции извлечения (Extract*)

В Django существуют специальные функции для работы с компонентами полей типа DateField, DateTimeField и TimeField. Эти функции позволяют фильтровать, аннотировать и агрегировать записи на основе отдельных частей даты или времени.

Функции Extract* находятся в модуле django.db.models.functions и выполняются на уровне базы данных — они не загружают все объекты в память Python.

Основные функции Extract*

Функция Что извлекает Пример значения Тип поля
ExtractYear Год из даты или времени 2023 Date, DateTime
ExtractMonth Месяц из даты или времени 1–12 Date, DateTime
ExtractDay День из даты или времени 1–31 Date, DateTime
ExtractWeekDay День недели (1=воскресенье, 7=суббота) 1–7 Date, DateTime
ExtractWeek Номер недели в году (ISO) 1–53 Date, DateTime
ExtractQuarter Квартал года 1–4 Date, DateTime
ExtractHour Час из времени 0–23 Time, DateTime
ExtractMinute Минута из времени 0–59 Time, DateTime
ExtractSecond Секунда из времени 0–59 Time, DateTime
Импорт: все функции импортируются из одного модуля:
from django.db.models.functions import (
    ExtractYear, ExtractMonth, ExtractDay,
    ExtractWeekDay, ExtractWeek, ExtractQuarter,
    ExtractHour, ExtractMinute, ExtractSecond
)

Два способа фильтрации по дате

1. Прямая фильтрация через встроенные lookups

Самый простой способ — использовать встроенные Django lookup-суффиксы: __year, __month, __day, __week, __quarter, __week_day, __hour, __minute, __second. Дополнительный импорт не нужен.

# Прямая фильтрация по году публикации
books_2023 = Book.objects.filter(published_date__year=2023)

# Прямая фильтрация по месяцу (январь = 1)
january_books = Book.objects.filter(published_date__month=1)

# Прямая фильтрация по кварталу (2-й квартал)
q2_books = Book.objects.filter(published_date__quarter=2)

2. Фильтрация и аннотация через функции Extract*

Более гибкий способ — аннотировать queryset вычисляемым полем через annotate(), а затем фильтровать по нему. Это позволяет использовать аннотированные значения в order_by(), values() и агрегациях.

from django.db.models.functions import ExtractYear, ExtractMonth, ExtractQuarter
from myapp.models import Book

# Аннотирование и фильтрация по году
books_2023 = Book.objects.annotate(
    year=ExtractYear('published_date')
).filter(year=2023)

# Аннотирование и фильтрация по месяцу
january_books = Book.objects.annotate(
    month=ExtractMonth('published_date')
).filter(month=1)

# Аннотирование и фильтрация по кварталу
q2_books = Book.objects.annotate(
    quarter=ExtractQuarter('published_date')
).filter(quarter=2)
Когда что использовать:
  • Прямые lookups (__year) — для простой фильтрации. Короче, не требует импорта.
  • annotate() + Extract* — когда нужно извлечённое значение использовать в GROUP BY, сортировке или других вычислениях.

Часть 2: Query Parameters с APIView

Query parameters (параметры запроса) — это дополнительные данные, передаваемые в URL после символа ?. Они разделяются символом &, а ключ и значение разделяются символом =.

Пример URL с query parameters

https://127.0.0.1:8000/books/?author=John&published_year=2023

Здесь author и published_year — параметры запроса. Параметр author имеет значение John, параметр published_year — значение 2023.

Основные применения query parameters

  • Фильтрация / поиск: ограничение результатов по значениям параметров
  • Сортировка: определение порядка отображения данных
  • Пагинация: разделение результатов на страницы

request.query_params в DRF

В Django REST Framework доступ к query parameters осуществляется через атрибут request.query_params. Это объект типа QueryDict — аналог request.GET в стандартном Django, но предпочтительный в DRF.

class BookListView(APIView):
    def get(self, request):
        # Получить значение параметра или None, если не передан
        author = request.query_params.get('author')
        year = request.query_params.get('pub_year')

        # Получить с дефолтным значением
        page_size = request.query_params.get('page_size', '10')
request.query_params vs request.GET: в DRF рекомендуется использовать request.query_params — это более явное и читаемое имя, соответствующее терминологии HTTP. Оба атрибута работают одинаково.

1. Фильтрация данных через query_params

Типичный паттерн: собираем непустые параметры в словарь filters и передаём его в filter(**filters). Это позволяет легко комбинировать несколько фильтров.

# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Book
from .serializers import BookDetailSerializer

class BookListView(APIView):
    def get(self, request):
        filters = {}

        author = request.query_params.get('author')
        published_year = request.query_params.get('pub_year')

        if author:
            filters['author'] = author

        if published_year:
            filters['published_date__year'] = published_year

        books = Book.objects.filter(**filters)
        serializer = BookDetailSerializer(books, many=True)
        return Response(serializer.data)

Пример запроса

GET http://127.0.0.1:8000/books/?author=George%20Orwell

Возвращает список книг автора George Orwell. Если параметры не переданы — возвращает все книги.

Важно: всегда проверяйте, что параметр не None перед добавлением в filters. Иначе filter(author=None) вернёт пустой queryset вместо ожидаемых всех записей.

2. Сортировка данных через query_params

Параметры sort_by и sort_order позволяют клиенту управлять сортировкой. В Django префикс - у имени поля означает сортировку по убыванию.

# views.py
class BookListView(APIView):
    def get(self, request):
        sort_by = request.query_params.get('sort_by', 'title')
        sort_order = request.query_params.get('sort_order', 'asc')

        books = Book.objects.all()

        if sort_order == 'desc':
            sort_by = f'-{sort_by}'

        books = books.order_by(sort_by)
        serializer = BookDetailSerializer(books, many=True)
        return Response(serializer.data)

Пример запроса

GET http://127.0.0.1:8000/books/?sort_by=price&sort_order=desc

Возвращает книги, отсортированные по цене в порядке убывания.

Безопасность: значение sort_by напрямую передаётся в order_by(). Без валидации допустимых полей клиент может передать произвольное имя поля. Рекомендуется белый список разрешённых полей:
ALLOWED_SORT_FIELDS = {'title', 'price', 'published_date', 'author'}
if sort_by not in ALLOWED_SORT_FIELDS:
    sort_by = 'title'  # дефолт
⚠️ Проверить по документации: для промышленного кода используйте OrderingFilter из DRF.

3. Пагинация через PageNumberPagination

Пагинация разделяет большие наборы данных на страницы. PageNumberPagination — стандартный класс DRF. Параметры page и page_size передаются через URL.

# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from .models import Book
from .serializers import BookDetailSerializer

class BookListView(APIView, PageNumberPagination):
    page_size = 5  # значение по умолчанию

    def get(self, request):
        books = Book.objects.all()
        page_size = self.get_page_size(request)  # читает page_size из query_params
        self.page_size = page_size               # устанавливает для этого запроса
        results = self.paginate_queryset(books, request, view=self)
        serializer = BookDetailSerializer(results, many=True)
        return self.get_paginated_response(serializer.data)

    def get_page_size(self, request):
        """Переопределение для поддержки параметра page_size."""
        page_size = request.query_params.get('page_size')
        if page_size and page_size.isdigit():
            return int(page_size)
        return self.page_size  # дефолт

Пример запроса

GET http://127.0.0.1:8000/books/?page=2&page_size=3

Возвращает вторую страницу списка книг с тремя элементами на странице.

Структура ответа с пагинацией

{
    "count": 25,          // всего объектов
    "next": "http://.../?page=3",
    "previous": "http://.../?page=1",
    "results": [...]      // объекты текущей страницы
}
Ключевые методы PageNumberPagination:
  • paginate_queryset(queryset, request, view=None) — возвращает срез queryset для текущей страницы
  • get_paginated_response(data) — оборачивает данные в стандартный ответ с метаданными
  • get_page_size(request) — определяет размер страницы (можно переопределить)
⚠️ Проверить по документации: в DRF 3.15+ рекомендуется настраивать пагинацию через pagination_class на уровне Generic Views или глобально в settings.py, а не через множественное наследование от PageNumberPagination в APIView. Паттерн из лекции работает, но не является предпочтительным.