Добавляем связь many-to-many (многие ко многим)

Курс по Django: https://stepik.org/a/183363

Архив проекта: 28_sitewomen.zip

Пришло время познакомиться с еще одним типом связей Many To Many (многие ко многим). На данный момент мы уже знаем, что она позволяет описывать взаимосвязи между множеством данных двух разных таблиц. Очень частый пример, это реализация информационных тегов для статей. Например, на нашем сайте мы можем определить теги:

оскар; высокие; блондинки; брюнетки; олимпийская чемпионка

И так далее. Понятно, что с одной женщиной можно связать сразу несколько разных тегов и, наоброт, с отдельными тегами – несколько разных постов. Получается взаимосвязь многие ко многим.

Тэеггирование в Django уже реализовано в модуле django-taggit. И с подробной документацией можно ознакомиться на станице:

https://django-taggit.readthedocs.io/en/latest/

Вообще, вначале, прежде чем делать что-либо, посмотрите, есть ли уже готовый нужный вам модуль и только после этого начинайте изобретать собственный велосипед. Надо сказать для Django разработана масса различных приложений для большинства типовых задач. Ну а мы в качестве примера самостоятельно добавим функционал теггирования на наш сайт.

Вначале в файле women/models.py мы определим новую модель для списка тегов следующим образом:

class TagPost(models.Model):
    tag = models.CharField(max_length=100, db_index=True)
    slug = models.SlugField(max_length=255, unique=True, db_index=True)
 
    def __str__(self):
        return self.tag

А в модели Women создадим новое поле с помощью класса ManyToManyField:

tags = models.ManyToManyField('TagPost', blank=True, related_name='tags')

Здесь первым аргументом мы должны передать или ссылку на класс модели или имя этого класса в виде строки. Так как класс TagPost объявлен после класса Women, то я использую строку. Обратите внимание, что параметр on_delete здесь не указывается. Для поля ManyToManyField его прописывать не нужно.

https://docs.djangoproject.com/en/4.2/ref/models/fields/#manytomanyfield

После определения новой модели и изменения существующей нам нужно обновить их структуру в БД. Выполним уже знакомую команду:

python manage.py makemigrations

и применим новые миграции:

python manage.py migrate

Откроем программу SQLiteStudio и видим, что в итоге были добавлены две новых таблицы. Одна вполне ожидаемая women_tagpost, а вторая – вспомогательная с именем women_women_tags. Эта вторая таблица будет хранить связи между множеством записей таблицы women и tagpost. Именно так реализуются связи многие ко многим на уровне БД. При этом в самой таблице women не было добавлено ни одного нового поля. В этом нет необходимости.

Работа со связью many-to-many через ORM Django

После того, как связь описана и сформированы новые таблицы, давайте посмотрим, как с ними можно работать с помощью ORM фреймворка Django. Откроем консоль командой:

python manage.py shell_plus

и первым делом добавим новые записи в таблицу tagpost:

TagPost.objects.create(tag='Блондинки', slug='blonde')
TagPost.objects.create(tag='Брюнетки', slug='brunetky')
TagPost.objects.create(tag='Оскар', slug='oskar')
TagPost.objects.create(tag='Олимпиада', slug='olimpiada')
TagPost.objects.create(tag='Высокие', slug='visokie')
TagPost.objects.create(tag='Средние', slug='srednie')
TagPost.objects.create(tag='Низкие', slug='niskie')

Далее, давайте первой записи из таблицы women (Анджелина Джоли) определим теги: брюнетки; высокие; оскар. Для этого сохраним ссылку на объект записи с id=1:

a = Women.objects.get(pk=1)

Затем, сформируем ссылки на объекты нужных нам тегов. Это можно сделать или так:

tag_br = TagPost.objects.all()[1]

или сразу распаковать коллекцию с нужными тегами:

tag_o, tag_v = TagPost.objects.filter(id__in=[3, 5])

Теперь, смотрите, у объекта модели Women появился новый атрибут tags:

a.tags

Через него можно выполнять добавление новых тегов к посту. В нашем случае удобно воспользоваться методом set() для записи сразу нескольких тегов:

a.tags.set([tag_br, tag_o, tag_v])

Если нужно добавить только один тег, то для этого существует другой метод add():

a.tags.add(tag_br)

Причем, если тег для нашего поста уже добавлен, то новой записи в промежуточной таблице не будет, то есть, дублирование здесь исключается.

Для удаления тега можно воспользоваться методом remove():

a.tags.remove(tag_o)

И видим, что в промежуточной таблице теперь всего две записи.

Чтобы получить список всех тегов для текущей записи, можно воспользоваться очевидной командой:

a.tags.all()

Или, наоборот, по тегу получить все посты, которые с ним связаны:

tag_br.tags.all()

Здесь мы используем менеджер с именем tags, который определили в параметре related_name класса ManyToManyField. Соответственно, через него можно вызывать все те же самые методы. Например, для тега tag_br добавить второй пост:

b = Women.objects.get(pk=2)
tag_br.tags.add(b)

Теперь команда:

tag_br.tags.all()

возвратит две записи.

Вот так достаточно просто можно использовать поле ManyToManyField. В заключение отмечу один важный момент создания новых записей с тегами. Предположим, что мы бы хотели добавить в БД еще одну известную женщину певицу Ариану Гранде. Если это сделать командой:

Women.objects.create(title='Ариана Гранде', slug='ariana-grande', cat_id=2, tags=[tag_br, tag_v])

то получим ошибку, что мы не можем назначать теги еще не сформированной записи. То есть, той записи, у которой еще нет идентификатора. Для поля Many To Many это необходимая информация, так как именно идентификатор используется для связи поста с тегами и сохраняется в промежуточной таблице. Если этого идентификатора нет, то, конечно же, сформировать такую связь не представляется возможным. Отсюда и ошибка.

Правильно было бы поступить так. Сначала создать запись по этой певице:

w = Women.objects.create(title='Ариана Гранде', slug='ariana-grande', cat_id=2)

А уже потом назначить ей необходимые теги:

w.tags.set([tag_br, tag_v])

На следующем занятии мы добавим теги на сайт, используя рассмотренные команды ORM.

Курс по Django: https://stepik.org/a/183363

Видео по теме