Django admin: инлайны, действия и кастомные формы на практике
Стандартная админка Django закрывает CRUD, но ломается на связанных объектах, массовых операциях и валидации. Разбираем три инструмента, которые превращают её в рабочий бэкофис.

Django admin из коробки даёт список, форму создания и форму редактирования на каждую модель. Этого хватает ровно до момента, когда контент-менеджер спрашивает: «А почему я не вижу товары прямо на странице заказа?», «Можно ли отметить сразу 200 записей как обработанные?» и «Почему система дала сохранить заказ с отрицательной суммой?». Ниже — три механизма, которые закрывают эти три запроса: инлайны, действия и кастомные формы. Всё для Django 5.x, но API инлайнов и действий стабилен с 3.x и вряд ли изменится.
У каждого приёма есть цена: инлайны легко порождают лишние SQL-запросы, действия по умолчанию молча игнорируют валидацию модели, а кастомная форма может разойтись с логикой save(). Разберём, где именно.
Инлайны: связанные объекты на одной странице
Инлайн показывает объекты, у которых есть ForeignKey на редактируемую модель, прямо в её форме. Классический случай — заказ и позиции заказа. Без инлайна менеджер сохраняет заказ, потом уходит на другую страницу добавлять позиции по одной. С инлайном он делает всё в одном экране.
Есть два базовых класса: TabularInline (позиции строками в таблице) и StackedInline (каждая позиция блоком, как обычная форма). Табличный компактнее и подходит, когда полей немного.
# admin.py, Django 5.x
from django.contrib import admin
from .models import Order, OrderItem
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0 # не показывать пустые строки для новых позиций
fields = ("product", "quantity", "price")
readonly_fields = ("price",)
# подтягиваем связанный product одним JOIN, а не отдельным запросом на строку
autocomplete_fields = ("product",)
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
inlines = (OrderItemInline,)
list_display = ("id", "customer", "created_at")
extra = 0 — важная деталь. По умолчанию Django рисует три пустых формы для новых позиций; на модели с обязательными полями это путает менеджера и раздувает страницу. Ставьте ноль и добавляйте кнопкой «Добавить ещё».
Где инлайны бьют по производительности
Каждая строка инлайна с ForeignKey — потенциальный отдельный запрос к базе, если поле выводится как выпадающий список. Заказ на 50 позиций легко превращается в 50+ запросов только на отрисовку формы. Два способа лечения:
- autocomplete_fields — заменяет тяжёлый
<select>на поиск по AJAX. Обязательно требует, чтобы у целевой модели (Product) в еёModelAdminбыл заданsearch_fields, иначе Django выбросит ошибку при загрузке админки. - raw_id_fields — совсем простой вариант: показывает ID и ссылку на выбор во всплывающем окне, без запроса на весь справочник.
Инлайн удобен для менеджера и опасен для базы. Правило простое: если у связанной модели больше пары сотен записей, поле выбора внутри инлайна должно быть autocomplete или raw_id, а не обычным select.
Инвалидация кеша списка после сохранения инлайна — отдельная тема, здесь её не трогаем.
Действия: массовые операции над выборкой
Действие (action) — функция, которая получает выбранные в списке записи и делает с ними что-то оптом. Стандартное «Удалить выбранные» — тоже action. Свои действия регистрируются декоратором @admin.action.
# admin.py
from django.contrib import admin
from django.utils import timezone
@admin.action(description="Отметить как обработанные")
def mark_processed(modeladmin, request, queryset):
# queryset.update() — один SQL UPDATE на всю выборку
updated = queryset.update(
status="processed",
processed_at=timezone.now(),
)
modeladmin.message_user(request, f"Обработано записей: {updated}")
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ("id", "customer", "status")
actions = (mark_processed,)
Главная ловушка: update() не вызывает save()
Метод queryset.update() идёт в базу напрямую. Он не вызывает Model.save(), не триггерит сигналы pre_save/post_save и не запускает валидацию. Если у вас в save() пересчитывается поле или в сигнале отправляется письмо — при массовом действии через update() этого не произойдёт. Логика тихо не сработает, ошибок не будет.
Когда важна логика модели, придётся идти в цикл — ценой N запросов:
@admin.action(description="Пересчитать и обработать")
def process_with_logic(modeladmin, request, queryset):
count = 0
for order in queryset:
order.status = "processed"
order.recalc_total() # ваша бизнес-логика
order.save() # save() отрабатывает, сигналы летят
count += 1
modeladmin.message_user(request, f"Обработано: {count}")
Выбор между двумя вариантами прямой: update() — когда меняете простые поля и важна скорость; цикл с save() — когда завязана бизнес-логика и объектов немного. На выборке в десятки тысяч записей цикл повесит воркер, поэтому такое действие лучше отдавать в фоновую задачу (Celery, django-rq), а не выполнять синхронно в запросе.
Кастомные формы: валидация и вычисляемые поля
По умолчанию форма админки строится автоматически из модели. Как только нужна валидация уровня «поле A не может быть больше поля B» или дополнительное поле, которого нет в модели, — подключают свой ModelForm через атрибут form.
# forms.py
from django import forms
from .models import Order
class OrderAdminForm(forms.ModelForm):
class Meta:
model = Order
fields = "__all__"
def clean(self):
cleaned = super().clean()
discount = cleaned.get("discount")
total = cleaned.get("total")
if discount is not None and total is not None and discount > total:
raise forms.ValidationError(
"Скидка не может превышать сумму заказа"
)
return cleaned
# admin.py
from .forms import OrderAdminForm
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
form = OrderAdminForm
Валидатор из clean() сработает при сохранении через форму админки. Но помните: как и в случае с действиями, queryset.update() и импорт данных мимо формы эту проверку обойдут. Форма админки — не замена валидации на уровне модели или базы, а слой поверх неё.
Разные формы для создания и редактирования
Иногда при создании объекта поле обязательно, а при редактировании — только для чтения. Для этого есть get_form(): он получает obj, который равен None на странице создания.
class OrderAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if obj: # объект существует — режим редактирования
return ("customer",) # менять покупателя после создания нельзя
return ()
Как три механизма сочетаются
| Инструмент | Закрывает | Главный риск |
|---|---|---|
| Инлайн | Редактирование связанных объектов на одной странице | N+1 запросов без autocomplete/raw_id |
| Действие (update) | Быстрая массовая правка простых полей | Не вызывает save() и сигналы |
| Действие (цикл save) | Массовая правка с бизнес-логикой | Медленно, вешает воркер на больших выборках |
| Кастомная форма | Кросс-полевая валидация, доп. поля | Обходится при записи мимо формы |
Все три спокойно живут в одном ModelAdmin: инлайн для позиций, действие для смены статуса пачкой, форма для валидации скидки. Конфликтов между ними нет — они работают на разных этапах жизненного цикла записи.
Что делать дальше
Прежде чем расширять админку, включите в разработке django-debug-toolbar и посмотрите на счётчик SQL-запросов при открытии страницы с инлайнами — это первое, что покажет проблему N+1. Дальше:
- перенесите долгие действия в фоновую очередь, если выборка может быть большой;
- критичную валидацию продублируйте на уровне модели (
clean()модели) или ограничением базы, а не только в форме; - для сложных инлайнов посмотрите в сторону
list_select_relatedиget_queryset()сprefetch_related.
Точные сигнатуры get_form(), get_formset() и полный список атрибутов инлайнов сверяйте с документацией версии, которую используете, — набор опций между релизами Django пополняется.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Django documentation — The Django admin site, Django documentation — Admin actions
Частые вопросы
У меня Django 3.2, а не 5.x — примеры сработают?
Действие сработало, но save() в модели не отработал — почему?
Админка падает с ошибкой при загрузке после добавления autocomplete_fields — что не так?
Валидация в clean() формы не срабатывает при импорте данных — это нормально?
Страница заказа с инлайном открывается по несколько секунд — что делать?
Можно ли в одном ModelAdmin использовать и инлайны, и действия, и кастомную форму сразу?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.