Настроить админку Django под свои модели без боли
Практическое руководство по настройке Django admin: list_display, поиск, фильтры, inline-модели и оптимизация запросов. Для тех, кто уже поднял проект и хочет удобную панель без сторонних пакетов.

Django admin вы получаете бесплатно вместе с фреймворком, но по умолчанию он показывает список объектов как <Order: Order object (17)>, без поиска, без фильтров и с проблемой N+1 на каждом внешнем ключе. Разберём, как за пару часов довести панель до состояния, в котором ей можно пользоваться каждый день: настроить колонки, поиск, фильтры, встроенные формы связанных моделей и убрать лишние SQL-запросы. Всё описанное работает в Django 5.x, ключевые атрибуты (list_display, list_filter, search_fields) существуют в неизменном виде начиная с ранних версий 3.x.
Задача не в том, чтобы знать все опции ModelAdmin наизусть, а в том, чтобы понимать, какие из них закрывают 90% реальных потребностей и чем каждая платит по производительности.
Модели для примера
Возьмём типичный набор: заказ, клиент и позиции заказа. На нём видно и связи, и то, где админка начинает тормозить.
# models.py — Django 5.x
from django.db import models
class Customer(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
def __str__(self):
return self.name
class Order(models.Model):
class Status(models.TextChoices):
NEW = "new", "Новый"
PAID = "paid", "Оплачен"
SHIPPED = "shipped", "Отправлен"
customer = models.ForeignKey(
Customer, on_delete=models.PROTECT, related_name="orders"
)
status = models.CharField(
max_length=20, choices=Status.choices, default=Status.NEW
)
total = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Заказ #{self.pk}"
class OrderItem(models.Model):
order = models.ForeignKey(
Order, on_delete=models.CASCADE, related_name="items"
)
title = models.CharField(max_length=200)
quantity = models.PositiveIntegerField(default=1)
price = models.DecimalField(max_digits=10, decimal_places=2)Колонки, поиск и фильтры
Первое, что бесит в дефолтной админке, — список из одинаковых строк. Основные атрибуты ModelAdmin закрывают это сразу.
# admin.py
from django.contrib import admin
from .models import Customer, Order, OrderItem
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
# колонки в списке; можно поле модели или метод ModelAdmin
list_display = ("id", "customer", "status", "total", "created_at")
# ссылка ведёт на карточку не только с id, но и с клиента
list_display_links = ("id", "customer")
# правая панель фильтров: по choices и по дате
list_filter = ("status", "created_at")
# поиск; двойное подчёркивание — по полю связанной модели
search_fields = ("id", "customer__name", "customer__email")
# сортировка по умолчанию
ordering = ("-created_at",)
# быстрое редактирование прямо в списке
list_editable = ("status",)
date_hierarchy = "created_at"Проверьте результат: откройте /admin/<приложение>/order/ — вверху появится строка поиска, справа блок фильтров, над таблицей — навигация по датам. Статус можно менять прямо в списке.
Осторожно с list_editable и list_display_links
Поле не может быть одновременно в list_editable и list_display_links — Django выдаст ошибку при загрузке. И если поле в list_editable, оно не должно быть первым в list_display, иначе по нему негде будет кликнуть для перехода в карточку.
Вычисляемые колонки
Часто нужно показать то, чего нет в модели напрямую: число позиций, цветной бейдж статуса, ссылку. Для этого — метод с декоратором @admin.display.
from django.utils.html import format_html
class OrderAdmin(admin.ModelAdmin):
list_display = ("id", "customer", "status_badge", "items_count", "total")
@admin.display(description="Позиций")
def items_count(self, obj):
# это поле — источник проблемы N+1, чиним ниже
return obj.items.count()
@admin.display(description="Статус")
def status_badge(self, obj):
colors = {"new": "gray", "paid": "green", "shipped": "blue"}
return format_html(
'<b style="color:{}">{}</b>',
colors.get(obj.status, "black"),
obj.get_status_display(),
)format_html экранирует подставляемые значения — не собирайте HTML через f-строку и mark_safe на пользовательских данных, это дыра в XSS.
Связанные модели: inline
Позиции заказа логично редактировать на той же странице, что и заказ. Для этого — inline-классы.
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0 # не показывать пустые формы "про запас"
min_num = 1 # хотя бы одна позиция обязательна
fields = ("title", "quantity", "price")
class OrderAdmin(admin.ModelAdmin):
inlines = (OrderItemInline,)TabularInline даёт компактную таблицу, StackedInline — форму на каждую запись (удобно, когда полей много). После этого на карточке заказа появится блок позиций с добавлением и удалением строк.
Где админка начинает тормозить
Список заказов с колонками customer и items_count на 200 строк выдаёт сотни SQL-запросов: один на список плюс по одному на клиента каждой строки (внешний ключ) плюс по одному на .count() позиций. Это классический N+1. Включите django-debug-toolbar или логирование SQL — и увидите картину.
Лечится двумя приёмами в переопределении get_queryset:
- select_related — для внешних ключей и один-к-одному, подтягивает связанное одним JOIN;
- prefetch_related — для обратных связей и many-to-many, делает второй запрос и склеивает в Python;
- агрегаты вместо
.count()в цикле — считаем в базе черезannotate.
from django.db.models import Count
class OrderAdmin(admin.ModelAdmin):
list_select_related = ("customer",) # короткий путь для FK
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(_items_count=Count("items"))
@admin.display(description="Позиций", ordering="_items_count")
def items_count(self, obj):
return obj._items_count # уже посчитано в запросеlist_select_related — то же, что select_related, но специально для списка админки; для простого FK его достаточно и не нужно переопределять get_queryset. Параметр ordering в декораторе делает вычисляемую колонку сортируемой по аннотации.
Правило простое: если колонка в list_display обращается к связанной модели, где-то рядом должен быть select_related, prefetch_related или annotate. Иначе каждая строка списка — отдельный поход в базу.
Сравнение приёмов оптимизации
| Приём | Для чего | Число запросов | Цена |
|---|---|---|---|
list_select_related | FK и OneToOne в колонках | 1 (JOIN) | Шире выборка, если полей много |
prefetch_related | Обратные FK, M2M | 2 (+склейка в Python) | Память под связанные объекты |
annotate(Count) | Счётчики, суммы | 0 дополнительных | Усложняет queryset |
.count() в методе | — | N (по строке на объект) | Так делать не надо |
Права и производительность на больших таблицах
Ещё два места, которые всплывают на реальных объёмах:
- raw_id_fields — для внешних ключей на таблицы с десятками тысяч записей. Обычный выпадающий список грузит все объекты в память;
raw_id_fields = ("customer",)заменяет его на поле с id и всплывающим поиском. - show_full_result_count = False — по умолчанию админка делает
COUNT(*)по всей таблице для пагинации. На миллионах строк это медленно; отключение убирает точный счётчик, но ускоряет открытие списка. - list_per_page — снижайте со 100 по умолчанию, если строки тяжёлые по рендеру.
Что делать дальше
Базовая настройка закрывает повседневную работу. Следующие шаги, когда упрётесь в них:
- подключите
django-debug-toolbarи проверьте число SQL-запросов на списках и карточках — это первое, что деградирует при росте данных; - для сложных прав доступа изучите
get_querysetс фильтрацией поrequest.userи переопределениеhas_change_permission; - кастомные действия над выборкой (массовая смена статуса) — через
actions, отдельная тема, здесь её не разворачиваем.
Не тяните сторонние пакеты вроде Grappelli или Jazzmin, пока штатная админка справляется: они меняют шаблоны и могут отстать от версии Django при обновлении.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: Django documentation — The Django admin site (ModelAdmin options), Django documentation — QuerySet API: select_related and prefetch_related
Частые вопросы
У меня Django 4.2, а не 5.x — код подойдёт?
Добавил колонку в list_display, а Django ругается ошибкой E108
Список всё равно делает много запросов, хотя я добавил list_select_related
Grozит ли что-то в проде, если оставить show_full_result_count включённым?
Можно ли редактировать поле прямо в списке для нескольких строк сразу?
Стоит ли ставить Jazzmin или django-grappelli для красивой админки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.