Django ORM

Custom managers и QuerySets | Курс Django ORM урок 8

Custom managers и QuerySets | Курс Django ORM урок 8
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 4

Цель урока

Научиться выносить бизнес-логику запросов в кастомные менеджеры и классы QuerySet. Разобрать разницу между Manager и QuerySet, паттерн as_manager(), реализацию soft delete и цепочку кастомных методов.

Необходимые знания


Проблема: логика запросов размазана по коду

Часто в проектах, одни и те же условия фильтрации дублируются в разных местах:

# views.py
products = Product.objects.filter(is_active=True, stock__gt=0)

# api/views.py
products = Product.objects.filter(is_active=True, stock__gt=0).select_related("category")

# management/commands/send_digest.py
products = Product.objects.filter(is_active=True, stock__gt=0).order_by("-created_at")[:10]

Если бизнес-логика "активный продукт в наличии" изменится (например, добавится проверка price__gt=0), нужно обновить все эти места. Custom managers решают это централизовано.


Manager vs QuerySet - разница

Manager: это интерфейс через который Django предоставляет QuerySet. Product.objects, это менеджер. Менеджер дает первый QuerySet.

QuerySet: это запрос, который можно фильтровать, аннотировать, чейнить. После Product.objects.all(), работает QuerySet.

Ключевое отличие: методы Manager доступны только как первый вызов Product.objects.method(), методы QuerySet, в цепочке Product.objects.all().method().another_method().


Custom Manager

class ProductManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_active=True)

    def in_stock(self):
        return self.get_queryset().filter(stock__gt=0)

    def featured(self):
        return self.get_queryset().filter(is_featured=True).order_by("-created_at")


class Product(models.Model):
    name = models.CharField(max_length=200)
    is_active = models.BooleanField(default=True)
    stock = models.IntegerField(default=0)
    is_featured = models.BooleanField(default=False)

    objects = ProductManager()  # заменяем стандартный менеджер


# Теперь:
Product.objects.all()           # только активные (переопределен get_queryset)
Product.objects.in_stock()      # активные и в наличии
Product.objects.featured()      # активные избранные


-- Product.objects.all()
SELECT * FROM shop_product WHERE is_active = true ORDER BY name;

-- Product.objects.in_stock()
SELECT * FROM shop_product WHERE is_active = true AND stock > 0 ORDER BY name;

Несколько менеджеров

Часто нужен доступ ко всем объектам (например, в Django Admin):

class Product(models.Model):
    objects = ProductManager()       # только активные (дефолтный)
    all_objects = models.Manager()   # все, включая неактивные


Product.objects.all()          # только активные
Product.all_objects.all()      # все продукты

Django Admin использует default_manager модели. Если первый менеджер фильтрует объекты, Admin покажет только их. Явно укажите для Admin через Meta.default_manager_name или используйте all_objects как второй менеджер.


Custom QuerySet

Чтобы методы были доступны в цепочке, определите их в QuerySet:

class ProductQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)

    def in_stock(self):
        return self.filter(stock__gt=0)

    def with_reviews(self):
        return self.annotate(review_count=Count("reviews"))

    def expensive(self, threshold=1000):
        return self.filter(price__gte=threshold)

    def for_category(self, category_slug):
        return self.filter(category__slug=category_slug)

as_manager() - QuerySet как Manager

Метод as_manager() создает Manager из QuerySet. Это рекомендованный способ:

class Product(models.Model):
    name = models.CharField(max_length=200)
    is_active = models.BooleanField(default=True)
    stock = models.IntegerField(default=0)
    price = models.DecimalField(max_digits=10, decimal_places=2)

    objects = ProductQuerySet.as_manager()


# Методы QuerySet доступны через менеджер
Product.objects.active()
Product.objects.in_stock()

# И в цепочке
Product.objects.active().in_stock().expensive(500)
Product.objects.for_category("phones").active().with_reviews().order_by("-review_count")


-- Product.objects.for_category("phones").active().in_stock()
SELECT shop_product.*
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
WHERE shop_category.slug = 'phones'
  AND shop_product.is_active = true
  AND shop_product.stock > 0
ORDER BY shop_product.name;

Паттерн: Manager + QuerySet вместе

Иногда нужны и кастомный get_queryset() (для глобальной фильтрации) и методы:

class ProductQuerySet(models.QuerySet):
    def in_stock(self):
        return self.filter(stock__gt=0)

    def with_stats(self):
        return self.annotate(
            review_count=Count("reviews"),
            avg_rating=Avg("reviews__rating"),
        )


class ProductManager(models.Manager):
    def get_queryset(self):
        return ProductQuerySet(self.model, using=self._db).filter(is_active=True)

    def in_stock(self):
        return self.get_queryset().in_stock()

    def with_stats(self):
        return self.get_queryset().with_stats()


class Product(models.Model):
    objects = ProductManager()
    all_objects = models.Manager()  # без фильтрации


# Менеджер + цепочка
Product.objects.in_stock().with_stats().order_by("-avg_rating")

Soft Delete - удаление без удаления

Soft delete, паттерн при котором объекты не удаляются из БД, а помечаются как удаленные:

from django.db import models
from django.db.models import QuerySet
from django.utils import timezone


class SoftDeleteQuerySet(QuerySet):
    def delete(self):
        """Мягкое удаление, пометить как удаленные."""
        return self.update(
            deleted_at=timezone.now(),
            is_deleted=True,
        )

    def hard_delete(self):
        """Настоящее удаление из БД."""
        return super().delete()

    def alive(self):
        return self.filter(is_deleted=False)

    def dead(self):
        return self.filter(is_deleted=True)


class SoftDeleteManager(models.Manager):
    def get_queryset(self):
        return SoftDeleteQuerySet(self.model, using=self._db).filter(is_deleted=False)


class DeletedObjectsManager(models.Manager):
    def get_queryset(self):
        return SoftDeleteQuerySet(self.model, using=self._db).filter(is_deleted=True)


class SoftDeleteModel(models.Model):
    """Абстрактная модель с soft delete."""
    is_deleted = models.BooleanField(default=False, db_index=True)
    deleted_at = models.DateTimeField(null=True, blank=True)

    objects = SoftDeleteManager()              # только не удаленные
    all_objects = models.Manager()             # все, включая удаленные
    deleted_objects = DeletedObjectsManager()  # только удаленные

    class Meta:
        abstract = True

    def delete(self, using=None, keep_parents=False):
        """Мягкое удаление объекта.

        using это алиас БД для проектов с несколькими базами данных.
        keep_parents нужен для Multi-table inheritance: если True, строка
        родительской таблицы не удаляется. Принимаем для совместимости
        с сигнатурой Model.delete(), но не используем при soft delete.
        """
        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.save(update_fields=["is_deleted", "deleted_at"])

    def hard_delete(self, using=None, keep_parents=False):
        """Настоящее удаление. Параметры пробрасываются в super().delete()."""
        super().delete(using=using, keep_parents=keep_parents)

    def restore(self):
        """Восстановить удаленный объект."""
        self.is_deleted = False
        self.deleted_at = None
        self.save(update_fields=["is_deleted", "deleted_at"])


class Product(SoftDeleteModel):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    # ... остальные поля


# Использование
product = Product.objects.get(id=1)
product.delete()              # мягкое удаление (is_deleted=True)
product.hard_delete()         # настоящее удаление

Product.objects.all()         # только не удаленные
Product.all_objects.all()     # все

# QuerySet soft delete
Product.objects.filter(price__lt=10).delete()  # мягкое удаление группы

# Восстановить
product.restore()             # is_deleted=False


-- product.delete()
UPDATE shop_product
SET is_deleted = true, deleted_at = NOW()
WHERE id = 1;

-- Product.objects.all()
SELECT * FROM shop_product
WHERE is_deleted = false
ORDER BY name;

Проблема: related objects при soft delete

select_related и prefetch_related не знают о soft delete, они возвращают все связанные объекты включая удаленные:

# Вернет Order у которого продукт помечен как удаленный
order = Order.objects.prefetch_related("items__product").first()
for item in order.items.all():
    print(item.product) # "удаленный" продукт

Решение через limit_choices_to в ForeignKey или явная фильтрация:

# Явная фильтрация в запросе
Order.objects.prefetch_related(
    Prefetch(
        "items",
        queryset=OrderItem.objects.select_related("product").filter(product__is_active=True)
    )
)

Пример: QuerySet для модели Order

Посмотрим как выглядит QuerySet для реальной бизнес-модели. Order имеет статус, пользователя, дату создания и позиции с ценой и количеством. Все запросы к заказам выносим в методы QuerySet:

class OrderQuerySet(models.QuerySet):
    def pending(self):
        return self.filter(status="pending")

    def delivered(self):
        return self.filter(status="delivered")

    def recent(self, days=30):
        from django.utils import timezone
        from datetime import timedelta
        cutoff = timezone.now() - timedelta(days=days)
        return self.filter(created_at__gte=cutoff)

    def for_user(self, user):
        return self.filter(user=user)

    def with_total(self):
        from django.db.models import Sum, F as F_expr, ExpressionWrapper, DecimalField
        return self.annotate(
            computed_total=Sum(
                ExpressionWrapper(
                    F_expr("items__price") * F_expr("items__quantity"),
                    output_field=DecimalField(max_digits=12, decimal_places=2),
                )
            )
        )


class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="orders")
    status = models.CharField(max_length=20)
    total_price = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

    objects = OrderQuerySet.as_manager()


# Использование:
Order.objects.for_user(user).recent(7).pending()
Order.objects.delivered().with_total().order_by("-computed_total")[:10]

Практическое задание

1) Создайте ProductQuerySet с методами: active(), in_stock(), by_category(slug), price_range(min, max), with_review_stats() (аннотация count и avg). Подключите через as_manager().

2) Реализуйте SoftDeleteModel как абстрактную модель. Примените к Review, при review.delete(), помечать как удаленный, не удалять из БД. Проверьте что Review.objects.all() не возвращает удаленные.

3) Добавьте менеджер all_objects = models.Manager() к Product. Проверьте что Django Admin использует objects (только активные) или настройте через Meta.default_manager_name.

4) Реализуйте OrderQuerySet с методами for_user(), recent(), with_total(). Напишите запрос: заказы пользователя за последние 7 дней с вычисленной суммой, отсортированные по сумме.

5) Напишите тест который проверяет:

  • Product.objects.active() не возвращает неактивные продукты
  • Soft delete: после product.delete() объект недоступен через objects, но доступен через all_objects
  • restore() возвращает объект в objects

Возможные ошибки

Manager вместо цепочки методов QuerySet

# Метод в Manager не работает в цепочке
class ProductManager(models.Manager):
    def active(self):
        return self.filter(is_active=True)

Product.objects.active().in_stock()  # AttributeError, in_stock не определен
                                      # active() вернул QuerySet без метода in_stock

Методы, которые нужно вызывать в цепочке, определяйте в QuerySet, не в Manager.

Переопределение get_queryset без all_objects

class ProductManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_active=True)

# Нет доступа к неактивным продуктам!
# Django Admin покажет только активные
# Миграции и другие утилиты Django могут работать некорректно

Всегда добавляйте all_objects = models.Manager() как запасной менеджер.

Soft delete без переопределения QuerySet.delete()

class SoftDeleteModel(models.Model):
    def delete(self):
        self.is_deleted = True
        self.save()

# Но QuerySet.delete() обходит Model.delete()!
Product.objects.filter(price__lt=10).delete()  # настоящее удаление

При soft delete обязательно переопределять и Model.delete() и QuerySet.delete().

as_manager() теряет кастомный get_queryset

class ProductQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)

objects = ProductQuerySet.as_manager()
# as_manager() создает Manager с обычным get_queryset()
# Нет автоматической фильтрации, только явные методы

Если нужен кастомный get_queryset(), используйте Manager + QuerySet, не только as_manager().


Связь со следующим уроком

В уроке 9.1 разберем наследование моделей: Abstract, Multi-table и Proxy. Когда каждый тип применять, какой SQL генерируется и почему выбор типа наследования влияет на производительность запросов.


<< Урок 7.3

Урок 9.1 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы