Что такое связи многие-ко-многим и когда их использовать
Связи многие-ко-многим (ManyToManyField, M2M) в Django ORM позволяют ассоциировать одну запись модели с несколькими записями другой модели, и наоборот. Классические примеры: статьи и теги (статья может иметь много тегов, тег может быть присвоен многим статьям), пользователи и группы, рекламные кампании и ключевые слова.
Эти связи незаменимы для моделирования сложных взаимосвязей данных, где объекты могут принадлежать к нескольким категориям или иметь множественные атрибуты, представленные другими моделями.
Проблема отображения связей многие-ко-многим в Django Admin по умолчанию
По умолчанию, Django Admin использует стандартный HTML-виджет <select multiple> для отображения и редактирования M2M-полей. Этот виджет становится крайне неудобным и непроизводительным при работе с большим количеством связанных объектов.
Выбор нескольких элементов из длинного списка с использованием Ctrl/Shift-клика — плохой UX. Кроме того, загрузка и отображение тысяч опций в <select> может существенно замедлить загрузку страницы администратора.
Цель статьи: отображение связей многие-ко-многим в удобном формате
Цель этой статьи — рассмотреть встроенные и кастомные способы улучшения отображения и управления M2M-связями в интерфейсе Django Admin, делая его более удобным и производительным для разработчиков и контент-менеджеров.
Использование ‘filter_horizontal’ и ‘filter_vertical’
Django Admin предлагает два встроенных виджета, которые значительно улучшают стандартный <select multiple>.
Настройка ModelAdmin для использования ‘filter_horizontal’ или ‘filter_vertical’
Для активации этих виджетов достаточно указать имя M2M-поля в атрибутах filter_horizontal или filter_vertical вашего класса ModelAdmin.
# models.py
from django.db import models
class Keyword(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self) -> str:
return self.name
class AdCampaign(models.Model):
name = models.CharField(max_length=200)
keywords = models.ManyToManyField(Keyword, blank=True)
# ... другие поля
def __str__(self) -> str:
return self.name
# admin.py
from django.contrib import admin
from .models import AdCampaign, Keyword
@admin.register(Keyword)
class KeywordAdmin(admin.ModelAdmin):
search_fields = ('name',)
@admin.register(AdCampaign)
class AdCampaignAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
# Используем filter_horizontal для поля 'keywords'
filter_horizontal: tuple[str, ...] = ('keywords',) # type: ignore
# Или альтернативно:
# filter_vertical: tuple[str, ...] = ('keywords',) # type: ignore
Преимущества и недостатки ‘filter_horizontal’ и ‘filter_vertical’
Преимущества:
Значительно лучший UX по сравнению со стандартным <select multiple>.
Позволяет легко перемещать элементы между списками ‘Доступные’ и ‘Выбранные’.
Включает поиск/фильтрацию, что упрощает навигацию по большому количеству опций.
Просты в интеграции – требуют всего одной строки конфигурации в ModelAdmin.
Недостатки:
Могут все еще быть медленными, если количество связанных объектов исчисляется многими тысячами, так как все опции загружаются на страницу (хотя и скрыты до фильтрации).
Занимают значительное пространство на экране, особенно filter_vertical.
Пример использования ‘filter_horizontal’ для полей многие-ко-многим
В приведенном выше примере для модели AdCampaign поле keywords настроено с использованием filter_horizontal. Это предоставляет интерфейс с двумя блоками: слева – доступные ключевые слова (с поиском), справа – выбранные для данной кампании. Пользователь может легко перемещать ключевые слова между блоками.
Применение ‘raw_id_fields’
Когда количество связанных объектов становится очень большим, filter_horizontal может быть недостаточно производительным. raw_id_fields предлагает альтернативный подход.
Настройка ModelAdmin для использования ‘raw_id_fields’
Аналогично filter_horizontal, raw_id_fields настраивается путем добавления имени M2M-поля (или ForeignKey) в соответствующий кортеж в ModelAdmin.
# admin.py
from django.contrib import admin
from django.contrib.auth.models import User, Permission
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
# Пример для стандартной модели User
class UserAdmin(BaseUserAdmin):
# Отображаем поля user_permissions и groups как raw_id_fields
raw_id_fields: tuple[str, ...] = ('user_permissions', 'groups',)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
# Для кастомной модели
# @admin.register(LargeDatasetModel)
# class LargeDatasetModelAdmin(admin.ModelAdmin):
# raw_id_fields = ('related_items',)
Когда ‘raw_id_fields’ является хорошим выбором (большое количество связанных объектов)
raw_id_fields идеально подходит в следующих случаях:
Связанная модель содержит тысячи или миллионы записей.
Необходимо минимизировать время загрузки страницы администратора.
Пользователю достаточно видеть ID связанных объектов или выбирать их через всплывающее окно поиска.
Пример использования ‘raw_id_fields’ для оптимизации интерфейса
Вместо отображения списка всех объектов, raw_id_fields показывает текстовое поле, содержащее ID выбранных связанных объектов (через запятую для M2M). Рядом с полем находится иконка лупы, которая открывает всплывающее окно со списком связанных объектов, поддерживающее поиск и пагинацию. Это значительно ускоряет загрузку и работу со страницей, содержащей M2M-связи с огромным числом потенциальных опций, например, привязка пользователя к специфическим разрешениям (Permission) из сотен доступных.
Кастомизация формы администратора
Для более тонкой настройки и использования сторонних виджетов можно кастомизировать форму, используемую в ModelAdmin.
Создание собственной формы ModelForm
Сначала создается класс формы, наследуемый от django.forms.ModelForm.
# forms.py
from typing import Any
from django import forms
from .models import AdCampaign, Keyword
class AdCampaignForm(forms.ModelForm):
class Meta:
model = AdCampaign
fields = '__all__'
# Здесь можно будет переопределить виджет для 'keywords'
Переопределение поля формы для связей многие-ко-многим
В классе формы можно напрямую переопределить поле, отвечающее за M2M-связь, указав другой виджет или логику.
Использование виджетов для более удобного выбора связанных объектов (Select2, Chosen)
Библиотеки вроде django-select2 или django-autocomplete-light предоставляют продвинутые виджеты с автодополнением, поиском на стороне сервера (AJAX) и улучшенным UX для выбора M2M-связей.
Пример: Интеграция виджета Select2 для связей многие-ко-многим
Предположим, установлен пакет django-select2.
# forms.py
from typing import Any
from django import forms
from django_select2 import forms as s2forms # Импорт из django-select2
from .models import AdCampaign, Keyword
class KeywordSelect2MultipleWidget(s2forms.ModelSelect2MultipleWidget):
# Указываем модель и поле для поиска
search_fields = [
'name__icontains',
]
class AdCampaignForm(forms.ModelForm):
# Переопределяем поле keywords, используя виджет Select2
keywords = forms.ModelMultipleChoiceField(
queryset=Keyword.objects.all(),
widget=KeywordSelect2MultipleWidget,
required=False,
label='Ключевые слова (Select2)'
)
class Meta:
model = AdCampaign
fields = '__all__'
# admin.py
from django.contrib import admin
from .models import AdCampaign, Keyword
from .forms import AdCampaignForm # Импортируем нашу кастомную форму
@admin.register(Keyword)
class KeywordAdmin(admin.ModelAdmin):
search_fields = ('name',)
@admin.register(AdCampaign)
class AdCampaignAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
# Указываем админке использовать нашу кастомную форму
form = AdCampaignForm
Теперь поле keywords будет использовать виджет Select2, который обеспечивает поиск по мере ввода и загружает опции через AJAX, что эффективно даже для очень больших наборов ключевых слов.
Inline модели для отображения связей многие-ко-многим (через промежуточную модель)
Если M2M-связь содержит дополнительные данные (например, дату добавления тега к статье, порядок показа баннеров в группе), необходимо использовать явную промежуточную модель (through). Django Admin позволяет редактировать такие связи с помощью инлайнов.
Создание промежуточной модели для связи многие-ко-многим
Определим модели для Статей, Тегов и промежуточную модель ArticleTag, хранящую порядок.
# models.py
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self) -> str:
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
# Связь M2M через модель ArticleTag
tags = models.ManyToManyField(Tag, through='ArticleTag', blank=True)
def __str__(self) -> str:
return self.title
class ArticleTag(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
# Дополнительное поле в промежуточной модели
order = models.PositiveIntegerField(default=0)
class Meta:
unique_together = ('article', 'tag') # Гарантируем уникальность связи
ordering = ('order',) # Сортировка по умолчанию
Использование InlineModelAdmin для отображения и редактирования связей через промежуточную модель
Создаем класс инлайна (TabularInline или StackedInline) для промежуточной модели ArticleTag.
# admin.py
from django.contrib import admin
from .models import Article, Tag, ArticleTag
# Инлайн для управления связями Article-Tag
class ArticleTagInline(admin.TabularInline):
model = ArticleTag
# Указываем, какие поля из ArticleTag отображать/редактировать
fields = ('tag', 'order')
# raw_id_fields можно использовать и внутри инлайна!
raw_id_fields = ('tag',)
# Количество пустых форм для добавления новых связей
extra = 1
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
search_fields = ('name',)
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ('title',)
search_fields = ('title',)
# Добавляем инлайн в админку статьи
inlines = [ArticleTagInline]
Настройка InlineModelAdmin для управления отображением полей
В классе инлайна (ArticleTagInline) можно настроить:
model: Промежуточная модель.
fields или fieldsets: Какие поля промежуточной модели отображать.
extra: Количество пустых форм.
raw_id_fields: Применить raw_id_fields к полям ForeignKey внутри инлайна (например, для tag).
autocomplete_fields: Использовать виджеты с автодополнением для полей ForeignKey.
classes: Добавить CSS классы (например, 'collapse').
Пример: Отображение связей многие-ко-многим через промежуточную модель в виде inline
В примере выше, при редактировании объекта Article, под основной формой появится таблица (TabularInline), где каждая строка представляет собой связь с одним Tag. В строке можно выбрать тег (используя raw_id_fields для удобства, если тегов много) и указать значение поля order. Это позволяет управлять как самими связями, так и дополнительными данными этих связей непосредственно на странице основной модели.