Как конкретизировать промежуточную таблицу many-to-many в Django для добавления дополнительных полей
Что проверяют
Знаете ли вы параметр through и умеете ли потом работать с такой связью: создавать, фильтровать, читать дополнительные поля. Следом часто спрашивают, как перевести уже существующую связь на свою промежуточную модель без потери данных.
Как отвечать
Промежуточная модель это обычная модель с двумя ForeignKey и нужными полями, а связь объявляется через неё:
- объявление:
authors = models.ManyToManyField(Author, through="Authorship"), где вAuthorshipлежатbook,authorи, например,role; - уникальность: для своей модели Django не создаёт ограничение на пару сам, поэтому добавьте
UniqueConstraintпо полямbookиauthorвMeta.constraints, иначе дубли пройдут; through_fields: нужен, когда в промежуточной модели несколько внешних ключей на одну и ту же модель, и Django не может угадать, какие из них образуют связь;- создание: с Django 2.2
add(),create()иset()работают и с кастомной моделью, а значения обязательных полей передаются черезthrough_defaults={"role": "editor"}. Можно и напрямуюAuthorship.objects.create(...); - чтение полей связи:
book.authors.all()вернёт авторов безrole. Дополнительные поля берут из самой промежуточной модели:book.authorship_set.select_related("author"); - фильтрация:
Book.objects.filter(authorship__role="editor", authorship__author__name="Иванов"). Условия в одномfilter()относятся к одной строке связи.
В админке filter_horizontal для такой связи не работает, вместо него TabularInline на промежуточную модель.
Где спотыкаются
Меняют through у существующего поля и ждут, что makemigrations всё сделает. Django не умеет так изменить связь: нужна миграция с SeparateDatabaseAndState, где новая модель садится на старую таблицу, а новые поля добавляются отдельными операциями.
Разносят условия по двум вызовам .filter().filter(). Для многозначной связи это два отдельных джойна, и в выборку попадает книга, где редактор один автор, а Иванов другой.
Пытаются читать role из book.authors. Получают AttributeError и начинают городить второй запрос на каждую строку вместо одного select_related по промежуточной модели.