Skip to content

Latest commit

 

History

History
250 lines (163 loc) · 10.1 KB

File metadata and controls

250 lines (163 loc) · 10.1 KB

Модуль 4. Урок 22. Связь Many-to-One (многие к одному) с ForeignKey в Django

В реальных проектах редко бывает, чтобы все данные хранились в одной таблице.

Например, у нас в проекте Cinemahub есть фильмы, и у каждого фильма есть жанр — драма, комедия, фантастика и т.д.

Если бы мы добавляли жанр прямо в таблицу фильмов в виде текста (genre="драма"), то одна и та же информация повторялась бы десятки раз. Изменить название жанра? Придётся обновить все записи, где он встречается. Это неудобно, медленно и противоречит принципу нормализации данных — важному правилу в проектировании баз данных.


Идея связи «многие к одному»

Вместо дублирования жанра в каждой строке таблицы Movie, мы создаём отдельную таблицу Genre, где каждый жанр хранится один раз. А таблица фильмов просто ссылается на него — через внешний ключ (ForeignKey).

Таким образом:

  • Один жанр может быть связан с несколькими фильмами.
  • Один фильм принадлежит только одному жанру.

Это и есть отношение "многие к одному" (Many-to-One).


Определение ForeignKey

Поле ForeignKey связывает одну модель с другой. Оно требует обязательных аргументов:

  • to — модель, на которую указывает связь (например, Genre);
  • on_delete — что произойдёт с фильмами, если удалить жанр.

Пример: создаём модели для проекта Cinemahub

from django.db import models

class Genre(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


class Movie(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    year = models.IntegerField()
    genre = models.ForeignKey('Genre', on_delete=models.PROTECT)

    def __str__(self):
        return self.title

Разбор кода

  • Genre — таблица жанров (например, драма, боевик, комедия).
  • Movie — таблица фильмов, у каждого из которых есть поле genre, ссылающееся на Genre.
  • on_delete=models.PROTECT запрещает удалять жанр, если есть фильмы, связанные с ним. Это защищает базу от случайной потери данных.

Применяем миграции

После добавления полей выполним команды:

python manage.py makemigrations
python manage.py migrate

💡 Если появится ошибка: IntegrityError: NOT NULL constraint failed: cinemahub_movie.genre_id Это значит, что в таблице Movie уже есть записи, а поле genre не может быть пустым.

Решение: временно разрешим null=True при создании поля:

genre = models.ForeignKey('Genre', on_delete=models.PROTECT, null=True)

После миграции заполним жанры, а затем можем убрать null=True и сделать новую миграцию.


Проверка работы в Django shell

Откроем интерактивную оболочку:

python manage.py shell

Создадим несколько жанров:

from cinemahub.models import Genre, Movie

g1 = Genre.objects.create(name='Драма', slug='drama')
g2 = Genre.objects.create(name='Фантастика', slug='fantasy')

Теперь создадим фильмы и привяжем их к жанрам:

Movie.objects.create(title='1+1', description='История дружбы...', year=2011, genre=g1)
Movie.objects.create(title='Матрица', description='О реальности...', year=1999, genre=g2)

Проверим, как работает связь:

>>> m = Movie.objects.get(title='Матрица')
>>> m.genre.name
'Фантастика'

>>> g1.movie_set.all()
<QuerySet [<Movie: 1+1>]>

Обратите внимание: у жанра g1 появился атрибут movie_set — это обратная связь, создаваемая Django автоматически.


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

Поле on_delete определяет, что произойдет с фильмами при удалении жанра.

Вот основные варианты:

Значение Поведение
CASCADE Удаляет все фильмы, связанные с жанром.
PROTECT Запрещает удаление жанра, если к нему есть фильмы.
SET_NULL Обнуляет ссылку на жанр (genre=None). Нужно добавить null=True.
SET_DEFAULT Присваивает жанр по умолчанию.
SET(value) Устанавливает конкретный жанр, указанный пользователем.
DO_NOTHING Django не предпринимает действий (может вызвать ошибку в БД).

Проверим поведение PROTECT

Попробуем удалить жанр, который используется в фильме:

g1.delete()

Результат:

django.db.models.deletion.ProtectedError:
("Cannot delete some instances of model 'Genre' because they are referenced through a protected foreign key",)

Значит, защита работает — Django не даст удалить жанр, если есть фильмы, к нему привязанные.


Проверим поведение CASCADE

Изменим поле:

genre = models.ForeignKey('Genre', on_delete=models.CASCADE)

Сделаем миграцию и проверим снова:

g2 = Genre.objects.get(name='Фантастика')
g2.delete()

Теперь удалятся и все фильмы, связанные с этим жанром.

Если откроем страницу фильмов в браузере, связанных с этим жанром больше не будет.


Проверка результата в браузере

  1. Откройте панель администратора Django: http://127.0.0.1:8000/admin/

  2. Добавьте несколько жанров и фильмов.

  3. Попробуйте удалить жанр:

    • При PROTECT Django не даст удалить его.
    • При CASCADE удалятся и фильмы.

Так можно наглядно увидеть, как работает связь между таблицами.


Практические задания

🧩 Задание 1. Создайте новую модель Country (страна производства) и добавьте связь с фильмами: одна страна — много фильмов. Проверьте, как изменится структура базы данных после миграций.


🧩 Задание 2. Добавьте в модель Movie связь с моделью Country через ForeignKey с параметром SET_NULL. Удалите одну страну и проверьте, что произойдет с фильмами.


🧩 Задание 3. Создайте несколько фильмов и жанров, затем в Django shell получите:

  1. Все фильмы определенного жанра.
  2. Жанр конкретного фильма.
  3. Количество фильмов в каждом жанре (используя count()).

🧩 Задание 4. Откройте панель администратора, удалите один жанр при PROTECT, а затем измените на CASCADE и повторите — сравните результат.


Вопросы

  1. Что означает связь «многие к одному»?
  2. Для чего используется ForeignKey?
  3. Какую роль играет параметр on_delete?
  4. Чем отличаются режимы PROTECT и CASCADE?
  5. Что произойдет, если использовать SET_NULL, но не указать null=True?
  6. Как получить список всех фильмов определенного жанра через ORM?
  7. Как Django называет обратную связь от жанра к фильмам по умолчанию?
  8. Что произойдет, если удалить жанр при DO_NOTHING?
  9. Зачем нормализуют базу данных?
  10. Как можно проверить связь Many-to-One в админке?

Предыдущий урок | Следующий урок