Кастомные менеджеры в Django: типичные ошибки
Кастомный менеджер ломает миграции, теряет фильтры в related-запросах и портит админку. Разбираем четыре ошибки, из-за которых менеджеры ведут себя не так, как задумано.

Кастомный менеджер выглядит просто: наследуешь models.Manager, переопределяешь get_queryset, добавляешь пару методов. А потом обнаруживаешь, что Order.objects.all() в шаблоне возвращает только оплаченные заказы, тесты падают на несуществующих записях, а миграция ругается на менеджер, который вы даже не собирались использовать. Статья про четыре ошибки, которые всплывают именно в Django (проверено на 4.2 и 5.x) — где очевидное решение даёт неочевидную проблему.
Ошибка 1. Переопределили default-менеджер под фильтрацию
Самый частый паттерн новичка: хочу, чтобы мягко удалённые записи не попадали в выборки. Решение в лоб — переопределить get_queryset у менеджера по умолчанию.
class SoftDeleteManager(models.Manager):
def get_queryset(self):
# отфильтровываем помеченные как удалённые
return super().get_queryset().filter(is_deleted=False)
class Article(models.Model):
is_deleted = models.BooleanField(default=False)
objects = SoftDeleteManager()
Работает — ровно до первой миграции с данными и до первого места, где вам нужны все записи. Django использует default-менеджер (первый объявленный, либо тот, что назначен через Meta.default_manager_name) во внутренних операциях: сериализация в dumpdata, related-запросы, а до определённых случаев — и в миграциях. Отфильтровав get_queryset у objects, вы обрезали данные там, где этого не ждёте.
Default-менеджер — не место для бизнес-фильтров. Он должен видеть все строки таблицы. Фильтрацию выносите в отдельный менеджер, а неотфильтрованный оставляйте первым.
Правильнее — два менеджера, и default объявлен первым:
class ArticleQuerySet(models.QuerySet):
def alive(self):
return self.filter(is_deleted=False)
class Article(models.Model):
is_deleted = models.BooleanField(default=False)
# первый = default, видит всё
objects = models.Manager()
# отдельный менеджер для "живых" записей
alive = ArticleQuerySet.as_manager()
Теперь Article.objects видит всё, а Article.alive.all() — только неудалённые. Проверка: Article._meta.default_manager.name должно вернуть 'objects'.
Ошибка 2. Методы менеджера не переходят в цепочку
Вы написали удобный метод на менеджере:
class OrderManager(models.Manager):
def paid(self):
return self.filter(status='paid')
И столкнулись с тем, что Order.objects.paid() работает, а Order.objects.filter(user=user).paid() падает с AttributeError: 'QuerySet' object has no attribute 'paid'. Причина: метод живёт на менеджере, а filter() возвращает QuerySet — другой объект, который про ваш метод ничего не знает.
Правильное место для цепочечных методов — QuerySet, а не менеджер. Пишете методы на QuerySet и превращаете его в менеджер через as_manager():
class OrderQuerySet(models.QuerySet):
def paid(self):
return self.filter(status='paid')
def recent(self):
return self.order_by('-created_at')
class Order(models.Model):
status = models.CharField(max_length=20)
created_at = models.DateTimeField(auto_now_add=True)
objects = OrderQuerySet.as_manager()
Теперь чейнинг работает в любом порядке: Order.objects.filter(user=user).paid().recent(). Проверка: вызовите два метода подряд после filter — исключения быть не должно.
Если нужен и метод менеджера (например, создающий объект), и цепочечные методы queryset, используйте Manager.from_queryset():
class OrderManager(models.Manager.from_queryset(OrderQuerySet)):
def create_paid(self, **kwargs):
return self.create(status='paid', **kwargs)
Так методы OrderQuerySet доступны и на менеджере, и в цепочке, а create_paid — только на менеджере, что логично: чейнить его смысла нет.
Ошибка 3. Related-запросы обходят кастомный менеджер
Отфильтровали default-менеджер (вопреки первой ошибке) и удивляетесь, почему author.article_set.all() всё равно возвращает удалённые статьи. Дело в том, что для обратных связей и для ForeignKey-доступа Django по умолчанию использует не default-менеджер, а base-менеджер — обычно чистый models.Manager без фильтров. Это сделано намеренно: связь не должна молча терять объекты.
Заставить related-запросы использовать ваш менеджер можно через Meta.base_manager_name, но подумайте дважды. Если base_manager_name указывает на фильтрующий менеджер, вы получите тихие потери в related-доступе — ровно то, от чего Django вас защищает. Явное указание менеджера в запросе честнее:
- для конкретной связи фильтруйте вручную:
author.article_set(manager='alive').all()— если у связанной модели есть менеджерalive; - либо фильтруйте сам queryset:
author.article_set.filter(is_deleted=False); base_manager_nameменяйте только когда точно понимаете последствия для всей ORM.
Ошибка 4. use_in_migrations и вызовы БД внутри get_queryset
Если у менеджера выставлен use_in_migrations = True, Django сериализует его в файл миграции и вызывает при накатывании. Это полезно, когда в data-миграциях вы обращаетесь к историческим моделям и хотите те же методы. Но если get_queryset или методы менеджера обращаются к другим моделям, к текущему пользователю или к любому состоянию, которого в момент миграции ещё нет, — миграция падает или ведёт себя непредсказуемо.
Правило простое: менеджер, помеченный use_in_migrations, должен зависеть только от полей своей модели и не тянуть внешний контекст. Ниже — сравнение поведения менеджеров по назначению.
| Роль менеджера | Где используется Django | Можно фильтровать в get_queryset |
|---|---|---|
| Default (первый объявленный / default_manager_name) | Related-обвязка в шаблонах, часть внутренних операций | Нежелательно — должен видеть всё |
| Base (base_manager_name) | Обратные связи, ForeignKey-доступ, dumpdata | Нет — приведёт к тихим потерям |
| Обычный именованный (alive, published) | Только там, где вызываете явно | Да — для этого он и создан |
| С use_in_migrations = True | Плюс внутри миграций | Да, но без обращений к внешним моделям и контексту |
Если не получилось
- AttributeError: 'QuerySet' object has no attribute '...' — метод объявлен на менеджере, а вы вызываете его после
filter(). Перенесите метод вQuerySetи используйтеas_manager()илиfrom_queryset(). - В админке или dumpdata пропали записи — фильтрующий менеджер стал default или base. Верните чистый
models.Manager()первым в объявлении модели. - Related-запрос возвращает лишнее — обратная связь идёт через base-менеджер. Фильтруйте queryset явно вместо изменения
base_manager_name. - Миграция падает на вызове менеджера — у менеджера
use_in_migrations = Trueи внутри есть обращение к внешней модели или контексту. Уберите зависимость либо снимите флаг.
Что делать дальше
Проверьте в своих моделях, что первым объявлен неотфильтрованный менеджер, а вся бизнес-логика выборок живёт на QuerySet. Запустите python manage.py check и загляните в Model._meta.default_manager и base_manager у моделей с мягким удалением. Инвалидация фильтров при only/defer и поведение менеджеров с prefetch_related — отдельная тема, здесь мы её не разворачивали. Точное поведение base_manager_name и use_in_migrations сверяйте с документацией вашей версии Django, оно уточнялось между релизами.
Грант-менеджер: Заявки на ФСИ, Сколково, «Мой бизнес» — бюрократическим языком, со сметой.
Спросить за 20 ₽Источники: Django documentation — Managers, Django documentation — QuerySet API and as_manager()
Частые вопросы
Чем отличается as_manager() от from_queryset()?
У меня Django 3.2, а не 5.x — эти приёмы сработают?
Почему related-запрос игнорирует мой кастомный менеджер?
Можно ли сделать мягкое удаление одним фильтрующим менеджером по умолчанию?
Что будет, если поставить use_in_migrations на фильтрующий менеджер?
Если у модели несколько менеджеров, какой считается default?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.