2025, Dec 04 00:01

Почему Django m2m с through не добавляет дубликаты и что делать

Разбираем поведение Django m2m с явной through-моделью: почему add не создаёт вторую запись для пары и как добавлять строки через промежуточную модель.

Связи многие-ко-многим с явной промежуточной моделью могут неприятно удивить, если вы пытаетесь привязать одну и ту же пару сущностей несколько раз, меняя значения полей в through-модели. Типичный сценарий: вы связываете тему и систему, при этом в промежуточной таблице дополнительно хранится сервис. Первая вставка проходит, а вторая — с той же парой «тема–система», но другим сервисом — тихо игнорируется, без каких‑либо ошибок.

Минимальный пример поведения

Код ниже добавляет систему к теме через промежуточную модель с дополнительным полем. Исключений не возникает, однако повторный вызов с той же парой «тема–система» не создаёт новую строку в m2m-таблице.

from django.db import transaction

with transaction.atomic():
    topic_obj = ChatTopics.objects.get(ct_id=chat_topic_id)
    platform_obj = Systems.objects.get(sys_id=system_id)
    svc_obj = ServiceCatalog.objects.get(sc_id=service_id)

    topic_obj.ct_systems.add(
        platform_obj,
        through_defaults={"ctm_service": svc_obj}
    )

Что на самом деле происходит

Это ожидаемое поведение .add(…). Документированный контракт предельно прост:

Повторное добавление допустимо — связь дублироваться не будет

Иными словами, менеджер m2m воспринимает связь между конечными объектами (здесь: тема и система) как множество. Если строка, соединяющая ту же тему и ту же систему, уже есть, повторный вызов .add(…) не создаст новую запись — даже если в through_defaults указан другой сервис.

Как получить желаемый результат

Если вам нужны несколько строк для одной и той же пары «тема–система», отличающихся полями промежуточной модели, не полагайтесь на .add(…) менеджера m2m. Создавайте запись в through-модели напрямую. Это соответствует описанной логике и сводит работу к одному запросу.

from django.db import transaction

with transaction.atomic():
    ChatTopics_m2m.objects.create(
        ctm_system_id=system_id,
        ctm_chat_topic_id=chat_topic_id,
        ctm_service_id=service_id
    )

Поскольку здесь выполняется всего одна вставка, блок atomic, скорее всего, можно убрать.

Почему это важно

Понимание того, как менеджер many-to-many устраняет дубликаты, помогает избежать «тихих» холостых вызовов в путях записи. Вместо гаданий, почему новые строки не появляются в таблице связей, выбирайте подходящий API: .add(…) — когда вам нужны множества по паре конечных объектов, и прямые записи в through-модель — когда требуются несколько записей, отличающихся дополнительными полями.

Выводы

Если в m2m-связи есть дополнительные данные через явную through-модель, .add(…) не создаст вторую строку для той же пары конечных объектов. Для добавления записей, отличающихся through-полями, используйте прямую вставку в промежуточную модель и, при необходимости, упростите границы транзакции, так как операция — это один запрос.