JavaRush /Курси /Модуль 3: Django /Приклади Складних Фільтрів в API

Приклади Складних Фільтрів в API

Модуль 3: Django
Рівень 19 , Лекція 8
Відкрита

У цій лекції ми заглибимося у фільтрацію ще сильніше.

Розглянемо реальну ситуацію: ваш додаток — це платформа для онлайн-магазину з тисячами продуктів. Клієнти можуть шукати продукти за категорією, ціною, наявністю, рейтингом та іншими критеріями. При цьому вони можуть комбінувати умови, наприклад, "всі ноутбуки в категорії 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)
  1. Логічний оператор & — це AND.
  2. Оператор ~ — це 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. 😉

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ