Менеджеры и команды управления
Выносим повторяющиеся запросы в менеджеры и QuerySet, а разовые задачи — в свои команды manage.py.
Manager — это интерфейс к запросам модели (то самое
Model.objects), а QuerySet — ленивый, цепляемый набор объектов; кастомизируя их, вы даёте запросам понятные имена и переиспользуете логику.
Зачем это нужно на практике
По коду расползается один и тот же фильтр: Article.objects.filter(is_published=True, deleted=False) встречается в десятке view, в паре команд и в шаблонном теге. Это классическое дублирование, только спрятанное в запросах. Стоит изменить правило «что считать опубликованным» — скажем, добавить условие «и дата публикации уже наступила» — и нужно вручную найти и поправить все эти места. Что-нибудь обязательно пропустят, и на одной странице появятся статьи из будущего. Кастомные менеджеры и QuerySet решают ровно эту проблему: вы пишете Article.objects.published() в одном месте, и смысл запроса задан централизованно. Код становится читаемее, потому что в месте использования виден смысл («опубликованные»), а не механика фильтра, и надёжнее, потому что правило живёт в одной точке и меняется одним движением.
Это тот же принцип, по которому повторяющуюся логику выносят в функцию. Просто здесь «функция» — это метод запроса, привязанный к модели. И раз уж он привязан к модели, его естественно держать рядом с ней, а не растаскивать по view.
Кастомный QuerySet и его методы
Самый гибкий подход — описать методы прямо на QuerySet, потому что их результат можно сцеплять дальше. Идея в том, что у QuerySet есть приятное свойство: почти все его методы (filter, exclude, order_by) возвращают новый QuerySet. Если ваш метод тоже вернёт QuerySet, он встроится в эту механику и сможет стоять в середине цепочки. Каждый метод принимает self (текущий, ещё не выполненный queryset) и возвращает новый, отфильтрованный — он не трогает базу, а лишь уточняет будущий запрос:
from django.db import models
class ArticleQuerySet(models.QuerySet):
def published(self):
return self.filter(is_published=True, deleted=False)
def by_author(self, user):
return self.filter(author=user)
def popular(self):
return self.order_by("-views")
Чтобы эти методы стали доступны через objects, менеджер создают из QuerySet. Удобнее всего Manager.from_queryset — он «прокидывает» все методы QuerySet в менеджер автоматически:
class Article(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
is_published = models.BooleanField(default=False)
deleted = models.BooleanField(default=False)
views = models.IntegerField(default=0)
objects = ArticleQuerySet.as_manager() # методы QS доступны на objects
Теперь запросы читаются как фразы, и — главное — сцепляются, потому что каждый метод вернул QuerySet:
Article.objects.published() # все опубликованные
Article.objects.published().by_author(request.user) # его опубликованные
Article.objects.published().popular()[:5] # топ-5 популярных
Manager против QuerySet-методов
В чём разница и что выбрать? Метод на менеджере доступен только как начало цепочки: Article.objects.something(), но не ...published().something(). Метод на QuerySet доступен в любом месте цепочки и потому композируется. Правило простое: фильтры и сортировки кладите в QuerySet (чтобы их можно было комбинировать), а на менеджере оставляйте операции, которые логично начинать с модели (например, агрегаты или «создать с предзаполнением»). Иногда менеджер переопределяют целиком, чтобы изменить базовый набор — например, по умолчанию прятать «удалённые»:
class LiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(deleted=False)
class Article(models.Model):
# ...
objects = models.Manager() # все записи (первым — это менеджер по умолчанию)
live = LiveManager() # только не удалённые
Помните: первый объявленный менеджер становится менеджером по умолчанию (его использует админка и связи). Если первым поставить менеджер с фильтром, часть фреймворка «не увидит» отфильтрованные объекты — обычно первым держат полный objects.
Свои команды manage.py
Вторая опора продвинутого Django — собственные management-команды. Это скрипты, которые запускаются как python manage.py имя и при этом работают внутри полностью настроенного проекта: уже подгружены настройки, поднято подключение к базе, доступны все модели и ORM. Этим команда выгодно отличается от обычного скрипта, лежащего сбоку: не нужно вручную инициализировать Django, импортировать настройки, открывать соединение — фреймворк всё подготовил за вас. Команды идеальны для задач, которые не привязаны к HTTP-запросу: разовый импорт данных из CSV, ночная рассылка дайджеста, чистка устаревших записей, пересчёт денормализованных полей, любые регулярные операции по cron. По сути это «вход в ваш проект со стороны терминала».
Команда — это класс Command(BaseCommand) в файле приложение/management/commands/имя.py. Структура каталогов фиксированная, в обеих папках нужен __init__.py:
myapp/
management/
__init__.py
commands/
__init__.py
cleanup_drafts.py # запуск: python manage.py cleanup_drafts
Вся логика — в методе handle. Аргументы и опции описывают в add_arguments (Django использует стандартный argparse):
from django.core.management.base import BaseCommand
from myapp.models import Article
class Command(BaseCommand):
help = "Удаляет черновики старше N дней"
def add_arguments(self, parser):
parser.add_argument("--days", type=int, default=30)
parser.add_argument("--dry-run", action="store_true")
def handle(self, *args, **options):
from django.utils import timezone
from datetime import timedelta
edge = timezone.now() - timedelta(days=options["days"])
qs = Article.objects.filter(is_published=False, created__lt=edge)
count = qs.count()
if options["dry_run"]:
self.stdout.write(f"Будет удалено: {count}")
return
qs.delete()
self.stdout.write(self.style.SUCCESS(f"Удалено черновиков: {count}"))
Запуск с опциями: python manage.py cleanup_drafts --days 60 --dry-run. Тип --days объявлен как int, поэтому Django сам преобразует строку из командной строки в число и сам выдаст понятную ошибку, если передать не число. Опция --dry-run с action="store_true" — это флаг без значения: написали его — в options будет True, не написали — False. Обратите внимание на детали хорошего тона: вывод идёт через self.stdout.write (а не print), успех подсвечивают через self.style.SUCCESS зелёным, а флаг --dry-run позволяет безопасно посмотреть, сколько записей попадёт под нож, до того как реально их удалить. Для команд, которые что-то необратимо меняют, такой «холостой прогон» — почти обязательная практика.
Как это работает под капотом
Менеджер — это «дескриптор» на классе модели: обращение Article.objects возвращает менеджер, привязанный к модели, а его методы вроде filter создают QuerySet. QuerySet ленивый: цепочка .published().popular() только накапливает условия и не ходит в базу; SQL выполняется лишь при итерации, срезе или list()/count(). Поэтому сцеплять методы дёшево — это сборка запроса, а не множество обращений к БД. Команды же Django находит, сканируя папки management/commands во всех приложениях из INSTALLED_APPS; имя файла становится именем команды, а call_command("cleanup_drafts") позволяет вызвать её и из кода, например из теста.
Частые ошибки
- Фильтр-метод на менеджере вместо QuerySet — теряется возможность сцеплять (
...published().popular()не сработает). - Менеджер с фильтром поставили первым — он стал дефолтным, и админка/связи перестали видеть скрытые объекты.
- Нет
__init__.pyвmanagement/илиcommands/— команда не находится,manage.pyеё «не видит». printвместоself.stdoutв команде — ломает перенаправление вывода и тесты черезcall_command.
Итоги
- Кастомный QuerySet даёт именованные, сцепляемые фильтры;
QuerySet.as_manager()прокидывает их вobjects. - Фильтры — на QuerySet (композиция), «начинающие» операции — на менеджере; первый менеджер становится дефолтным.
- QuerySet ленив: цепочка собирает SQL и бьёт в базу только при итерации/срезе/
count(). - Команды живут в
management/commands/; логика вhandle, аргументы вadd_arguments. - В командах используйте
self.stdout/self.styleи флаг--dry-runдля безопасных операций.