В этой лекции мы углубимся в фильтрацию ещё сильнее.
Рассмотрим реальную ситуацию: ваше приложение — это платформа для онлайн-магазина с тысячами продуктов. Клиенты могут искать продукты по категории, цене, наличию, рейтингу и другим критериям. При этом они могут комбинировать условия, например, "все ноутбуки в категории electronics с рейтингом выше 4.5, стоимостью менее $1000 и бесплатной доставкой".
Как справиться с таким запросом, не потеряв производительности? Именно здесь на помощь приходят сложные фильтры, которые мы сегодня подробно разберем.
Использование Q-объектов для сложных условий фильтрации
Q-объекты в Django позволяют создавать сложные SQL-запросы с условиями AND, OR и NOT. Они обеспечивают гибкость при фильтрации данных, позволяя объединять несколько условий.
Давайте сначала реализуем простейший сценарий: вернуть из базы продукты, которые либо относятся к категории electronics, либо стоят меньше $500. Условие, как вы наверное догадались здесь будет соответствовать условию OR.
from django.db.models import Q
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Product
from .serializers import ProductSerializer
class ProductListView(APIView):
def get(self, request):
# Используем Q-объекты для фильтрации
products = Product.objects.filter(
Q(category='electronics') | Q(price__lt=500)
)
serializer = ProductSerializer(products, many=True)
return Response(serializer.data)
Здесь оператор | соответствует логическому OR. Наша фильтрация возвращает все продукты, которые относятся к категории electronics или имеют цену ниже $500.
Теперь давайте усложним: мы хотим найти продукты, которые относятся к категории books, имеют рейтинг выше 3, но при этом их цена должна быть больше $20 и они не должны быть распроданы.
class AdvancedProductListView(APIView):
def get(self, request):
products = Product.objects.filter(
Q(category='books') & Q(rating__gt=3) & Q(price__gt=20) & ~Q(is_on_sale=True)
)
serializer = ProductSerializer(products, many=True)
return Response(serializer.data)
- Логический оператор
&— это AND. - Оператор
~— это NOT, т.е. инвертирует условие.
Комбинирование Q-объектов с фильтрами QuerySet
Если вам нужно комбинировать сложную фильтрацию с базовой, это легко достигается добавлением обычных фильтров после применения Q-объектов.
Пример: фильтрация по связанным объектам
Предположим, у нас есть модель Order, связанная с пользователем и продуктами через ForeignKey. Мы хотим найти все заказы, сделанные пользователями из города New York, содержащие продукты из категории clothing и сделанные в последние 30 дней.
from datetime import timedelta
from django.utils.timezone import now
class OrderListView(APIView):
def get(self, request):
today = now()
last_30_days = today - timedelta(days=30)
orders = Order.objects.filter(
Q(user__city='New York') & Q(products__category='clothing') & Q(created_at__gte=last_30_days)
).distinct()
serializer = OrderSerializer(orders, many=True)
return Response(serializer.data)
Здесь мы видим сочетание:
Q(user__city='New York')для фильтрации по связанному объектуuser.Q(products__category='clothing')для фильтрации по связанным объектамproducts.Q(created_at__gte=last_30_days)для фильтрации по дате.
Метод distinct() убирает дубликаты результатов, которые могут появиться при фильтрации через связанные модели.
Сложные фильтрации с параметрами запроса
Давайте интегрируем сложные условия в API, принимая параметры фильтрации из URL. Это позволит конечным пользователям задавать критерии фильтрации динамически.
Предположим, у нас есть API для поиска продуктов, поддерживающий фильтрацию по цене, категории, рейтингу и наличию:
class DynamicProductFilterView(APIView):
def get(self, request):
# Получение параметров запроса
category = request.query_params.get('category')
min_price = request.query_params.get('min_price')
max_price = request.query_params.get('max_price')
min_rating = request.query_params.get('min_rating')
available = request.query_params.get('available')
# Начинаем создавать базовый QuerySet
products = Product.objects.all()
# Добавляем условие фильтрации, если параметр задан
if category:
products = products.filter(category=category)
if min_price:
products = products.filter(price__gte=min_price)
if max_price:
products = products.filter(price__lte=max_price)
if min_rating:
products = products.filter(rating__gte=min_rating)
if available is not None:
products = products.filter(is_available=available.lower() == 'true')
# Сериализация данных
serializer = ProductSerializer(products, many=True)
return Response(serializer.data)
Теперь вы можете делать запросы вроде:
GET /api/products?category=electronics&min_price=100&max_price=1000&min_rating=4.5
Работа с фильтрацией через DjangoFilterBackend
Для более сложных сценариев фильтрации рекомендуется использовать DjangoFilterBackend. Оно поддерживает создание кастомных фильтров прямо в коде.
Давайте создадим фильтр для модели Product с помощью filters.FilterSet.
from django_filters import rest_framework as filters
from .models import Product
class ProductFilter(filters.FilterSet):
min_price = filters.NumberFilter(field_name="price", lookup_expr='gte')
max_price = filters.NumberFilter(field_name="price", lookup_expr='lte')
min_rating = filters.NumberFilter(field_name="rating", lookup_expr='gte')
category = filters.CharFilter(field_name="category")
class Meta:
model = Product
fields = ['category', 'min_price', 'max_price', 'min_rating']
Теперь подключим этот фильтр к нашему представлению:
from rest_framework import generics
from django_filters.rest_framework import DjangoFilterBackend
from .serializers import ProductSerializer
class ProductListView(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = ProductFilter
Теперь фильтры автоматически применяются к параметрам из запроса, например:
GET /api/products?category=books&min_price=10&max_price=50&min_rating=4
Примеры комбинированной фильтрации
Для реализации более сложных комбинаций можно комбинировать DjangoFilterBackend, кастомные фильтры и Q-объекты.
В примере ниже мы фильтруем с использованием Q-объектов внутри FilterSet
class CombinedProductFilter(filters.FilterSet):
complex_filter = filters.BooleanFilter(method='filter_complex_logic')
class Meta:
model = Product
fields = ['category', 'price', 'rating']
def filter_complex_logic(self, queryset, name, value):
if value:
return queryset.filter(
Q(category='electronics') | Q(price__lte=100)
)
return queryset
Теперь при установке параметра complex_filter=true, выполняется наша сложная фильтрация.
Ошибки и типичные проблемы
Сложные запросы часто приводят к неожиданным результатам или высокой нагрузке на базу данных. Например, забытый метод distinct() при фильтрации через связанные объекты может привести к дублированию данных. Еще одна распространенная ошибка — выполнение сложных условий без индексации базы данных, что приводит к снижению производительности. Всегда профилируйте свои запросы и используйте инструменты вроде django-debug-toolbar.
На этом этапе вы должны чувствовать себя более уверенно при создании сложных фильтров в API. Если у вас еще остались вопросы, то знайте: Django и DRF — ваши друзья, и все, что вам нужно, есть в официальной документации DRF. 😉
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ