В реальных проектах редко бывает, чтобы все данные хранились в одной таблице.
Например, у нас в проекте Cinemahub есть фильмы, и у каждого фильма есть жанр — драма, комедия, фантастика и т.д.
Если бы мы добавляли жанр прямо в таблицу фильмов в виде текста (genre="драма"), то одна и та же информация повторялась бы десятки раз.
Изменить название жанра? Придётся обновить все записи, где он встречается.
Это неудобно, медленно и противоречит принципу нормализации данных — важному правилу в проектировании баз данных.
Вместо дублирования жанра в каждой строке таблицы Movie, мы создаём отдельную таблицу Genre, где каждый жанр хранится один раз.
А таблица фильмов просто ссылается на него — через внешний ключ (ForeignKey).
Таким образом:
- Один жанр может быть связан с несколькими фильмами.
- Один фильм принадлежит только одному жанру.
Это и есть отношение "многие к одному" (Many-to-One).
Поле ForeignKey связывает одну модель с другой. Оно требует обязательных аргументов:
to— модель, на которую указывает связь (например,Genre);on_delete— что произойдёт с фильмами, если удалить жанр.
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.titleGenre— таблица жанров (например, драма, боевик, комедия).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 и сделать новую миграцию.
Откроем интерактивную оболочку:
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 определяет, что произойдет с фильмами при удалении жанра.
Вот основные варианты:
| Значение | Поведение |
|---|---|
CASCADE |
Удаляет все фильмы, связанные с жанром. |
PROTECT |
Запрещает удаление жанра, если к нему есть фильмы. |
SET_NULL |
Обнуляет ссылку на жанр (genre=None). Нужно добавить null=True. |
SET_DEFAULT |
Присваивает жанр по умолчанию. |
SET(value) |
Устанавливает конкретный жанр, указанный пользователем. |
DO_NOTHING |
Django не предпринимает действий (может вызвать ошибку в БД). |
Попробуем удалить жанр, который используется в фильме:
g1.delete()Результат:
django.db.models.deletion.ProtectedError:
("Cannot delete some instances of model 'Genre' because they are referenced through a protected foreign key",)
Значит, защита работает — Django не даст удалить жанр, если есть фильмы, к нему привязанные.
Изменим поле:
genre = models.ForeignKey('Genre', on_delete=models.CASCADE)Сделаем миграцию и проверим снова:
g2 = Genre.objects.get(name='Фантастика')
g2.delete()Теперь удалятся и все фильмы, связанные с этим жанром.
Если откроем страницу фильмов в браузере, связанных с этим жанром больше не будет.
-
Откройте панель администратора Django: http://127.0.0.1:8000/admin/
-
Добавьте несколько жанров и фильмов.
-
Попробуйте удалить жанр:
- При
PROTECTDjango не даст удалить его. - При
CASCADEудалятся и фильмы.
- При
Так можно наглядно увидеть, как работает связь между таблицами.
🧩 Задание 1.
Создайте новую модель Country (страна производства) и добавьте связь с фильмами:
одна страна — много фильмов.
Проверьте, как изменится структура базы данных после миграций.
🧩 Задание 2.
Добавьте в модель Movie связь с моделью Country через ForeignKey с параметром SET_NULL.
Удалите одну страну и проверьте, что произойдет с фильмами.
🧩 Задание 3. Создайте несколько фильмов и жанров, затем в Django shell получите:
- Все фильмы определенного жанра.
- Жанр конкретного фильма.
- Количество фильмов в каждом жанре (используя
count()).
🧩 Задание 4.
Откройте панель администратора, удалите один жанр при PROTECT,
а затем измените на CASCADE и повторите — сравните результат.
- Что означает связь «многие к одному»?
- Для чего используется
ForeignKey? - Какую роль играет параметр
on_delete? - Чем отличаются режимы
PROTECTиCASCADE? - Что произойдет, если использовать
SET_NULL, но не указатьnull=True? - Как получить список всех фильмов определенного жанра через ORM?
- Как Django называет обратную связь от жанра к фильмам по умолчанию?
- Что произойдет, если удалить жанр при
DO_NOTHING? - Зачем нормализуют базу данных?
- Как можно проверить связь Many-to-One в админке?