У цій лекції ми заглибимося у фільтрацію ще сильніше.
Розглянемо реальну ситуацію: ваш додаток — це платформа для онлайн-магазину з тисячами продуктів. Клієнти можуть шукати продукти за категорією, ціною, наявністю, рейтингом та іншими критеріями. При цьому вони можуть комбінувати умови, наприклад, "всі ноутбуки в категорії 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. 😉
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ