Создание связи many-to-one многие к одному (ForeignKey)

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

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

На этом занятии воспользуемся классом ForeignKey, который необходим для организации связей «многие к одному» для отношений между постами и категориями, чтобы каждый пост был связан с одной конкретной категорией, а каждая категория со множеством статей. То есть, здесь слово «многие» относятся к категориям, а слово «один» - к постам. При этом модель category называют первичной, а модель women – вторичной.

Класс ForeignKey принимает два обязательных аргумента:

  • to – ссылка или строка класса первичной модели, с которой происходит связывание (в нашем случае это класс Category – модели для категорий);
  • on_delete – определяет тип ограничения при удалении внешней записи (в нашем примере – это удаление из первичной таблицы Category).

В свою очередь, опция on_delete может принимать следующие значения:

  • models.CASCADE – удаление всех записей из вторичной модели (например, Women), связанных с удаляемой категорией;
  • models.PROTECT – запрещает удаление записи из первичной модели, если она используется во вторичной (выдает исключение);
  • models.SET_NULL – при удалении записи первичной модели (Category) устанавливает значение foreign key в NULL у соответствующих записей вторичной модели (Women);
  • models.SET_DEFAULT – то же самое, что и SET_NULL, только вместо значения NULL устанавливает значение по умолчанию;
  • models.SET() – то же самое, только устанавливает пользовательское значение;
  • models.DO_NOTHING – удаление записи в первичной модели (Category) не вызывает никаких действий у вторичных моделей.

Подробнее о них можно почитать на странице документации:

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

Давайте, посмотрим на конкретном примере, как используется класс ForeignKey. Перейдем в файл women/models.py и определим еще одну модель для категорий:

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

Здесь все вам должно быть уже знакомо. Напомню, что параметр db_index указывает СУБД индексировать данное поле, чтобы поиск по нему происходил быстрее.

Далее, пропишем дополнительное поле cat_id во вторичной модели Women:

cat = models.ForeignKey('Category', on_delete=models.PROTECT)

(суффикс _id для формирования поля cat_id Django добавит автоматически).

Обратите внимание, мы класс первичной модели Category указали как строку, потому что модель Category в файле models.py записана после модели Women, поэтому, при попытке указать ссылку на этот класс возникнет ошибка. Только поэтому класс указан как строка. Но вообще, можно записывать и так, и так. Следующий параметр мы определили через функцию PROTECT (это ссылка на функцию, а не константа), которая запрещает удаление категорий, используемых во вторичной модели Women.

Создание таблиц в базе данных

Все, формально модели у нас определены и пришло время создать соответствующие таблицы в БД. Фактически, первую таблицу women нужно лишь модифицировать, добавив одно поле, а вторую таблицу создать целиком. Для этого нам надо сформировать новую миграцию командой:

python manage.py makemigrations

Но полноценно выполниться она не сможет. Я специально решил показать этот момент, чтобы вы лучше понимали процесс перестройки структуры таблиц в БД. Что здесь не так? Смотрите, у нас в таблице women должно добавиться новое поле cat_id, ссылающееся на запись первичной таблицы category. Но в таблице women уже есть записи и при добавлении этого поля оно оказывается для них пустым. СУБД запрещает такую операцию, так как это поле обязательно должно ссылаться на идентификатор записи из связанной таблицы. Отсюда и возникает эта проблема.

Как ее обойти? Давайте временно разрешим записывать в cat_id значение NULL. Это можно сделать через параметр null:

cat = models.ForeignKey('Category', on_delete=models.PROTECT, null=True)

Завершим предыдущую миграцию (выберем 2) и снова запустим команду:

python manage.py makemigrations

Теперь все прошло в штатном режиме и у нас сформировался еще один файл миграции под номером 0005. Выполним миграции, внесем изменения непосредственно в БД с помощью уже известной нам команды:

python manage.py migrate

Видим, что ошибок никаких нет. Откроем программу SQLiteStudio и видим две наши таблицы: women_category и women_women. Причем, в таблице women_women появилось новое поле cat_id со значениями NULL.

Все, таблицы сформированы и давайте теперь добавим категории в новую таблицу. Перейдем в терминал, выполним команду:

python manage.py shell_plus

и создадим две записи в таблице category (в оболочке shell_plus модели импортируются автоматически):

Category.objects.create(name='Актрисы', slug='aktrisy')
Category.objects.create(name='Певицы', slug='pevicy')

Затем, у всех записей таблицы women установим поле cat_id равным 1 (певицы). Сначала выбираем все записи:

w_list = Women.objects.all()

и обновляем их:

w_list.update(cat_id=1)

Теперь можно выйти из консоли, убрать аргумент null=True у класса ForeignKey и снова создать миграции:

python manage.py makemigrations

В появившемся меню выберем пункт 2 и миграция будет создана. Применим ее:

python manage.py migrate

Все, теперь внешний ключ cat_id у нас не может быть пустым.

Работа параметра models.PROTECT

Давайте убедимся в том, что мы не сможем удалить рубрики из первичной модели Category, если они используются во вторичной модели Women. Для этого снова перейдем в оболочку:

python manage.py shell_plus

и выберем запись с id равным 1, так как именно эта категория сейчас используется в постах:

c = Category.objects.get(pk=1)

И попробуем ее удалить:

c.delete()

Получим ошибку ProtectedError, как раз из-за значения models.PROTECT параметра on_delete. А вот категорию с id=2 удалить можно. Прежде я сохраню отдельно файл БД, чтобы потом быстро ее восстановить в исходном виде и выполню команды:

c = Category.objects.get(pk=2)
c.delete()

В этом случае никаких ошибок не возникло и запись из таблицы Category была успешно удалена.

Работа параметра models.CASCADE

Давайте еще посмотрим на порядок работы параметра models.CASCADE. Выйдем из оболочки. Пропишем новое значение для on_delete:

cat = models.ForeignKey('Category', on_delete=models.CASCADE)

Снова перейдем в оболочку и выполним команды:

c = Category.objects.get(pk=1)
c.delete()

Никаких ошибок не произошло и при удалении первой категории из таблицы Category были удалены также все записи из таблицы Women, связанные с этой первой категорией (то есть, все).

Вот принцип работы двух параметров PROTECT и CASCADE. Я думаю, теперь вам не сложно будет понять работу всех остальных параметров.

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

Видео по теме