Представим, что мы создаём проект Cinemahub — сайт о фильмах и сериалах.
У нас уже есть таблица с фильмами, где хранятся их названия, описание, год выпуска и рейтинг. Всё работает, пока данных немного.
Но что если мы захотим добавить информацию о жанрах, актёрах, режиссёрах, странах, категориях (фильмы / сериалы) и т. д.?
Наивный вариант — добавить все эти данные в таблицу Movie. Например, сделать поля:
genre = "драма, триллер"
actors = "Киану Ривз, Лоуренс Фишбёрн"
country = "США"
Поначалу это может показаться удобным, но со временем мы столкнёмся с проблемами:
- Дублирование данных: имена актёров, жанры и страны будут постоянно повторяться.
- Сложность изменений: если актёр сменил имя, придётся менять его во всех фильмах.
- Падение производительности: фильтрация по текстовым полям всегда медленнее, чем по числовым ключам.
Чтобы избежать этого, базы данных используют связи между таблицами — способ разложить данные по отдельным таблицам и связать их логически.
Такой подход называется нормализацией данных.
- уменьшает избыточность данных;
- упрощает редактирование (изменения в одной таблице автоматически влияют на связанные записи);
- ускоряет запросы и фильтрацию.
Django поддерживает три основных типа связей между моделями:
| Тип связи | Django-поле | Описание |
|---|---|---|
| Многие к одному | ForeignKey |
Один объект может принадлежать многим другим (пример: фильм → категория). |
| Многие ко многим | ManyToManyField |
Обе стороны могут иметь множество связей (пример: актёр ↔ фильм). |
| Один к одному | OneToOneField |
Каждая запись связана только с одной другой (пример: фильм ↔ детальная карточка). |
Один из самых частых типов связи.
Например, один жанр может содержать много фильмов, но каждый фильм принадлежит только одному жанру.
Создадим две модели: Genre и Movie.
# movies/models.py
from django.db import models
class Genre(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Movie(models.Model):
title = models.CharField(max_length=200)
year = models.PositiveIntegerField()
description = models.TextField()
genre = models.ForeignKey(Genre, on_delete=models.CASCADE)
def __str__(self):
return self.titleЧто здесь происходит:
- Каждому фильму соответствует один жанр.
- Поле
genre— это внешний ключ (ForeignKey) на таблицуGenre. - Аргумент
on_delete=models.CASCADEозначает: если жанр удалить, то все связанные фильмы тоже будут удалены.
💡 Это логично: если мы удаляем жанр «Драма», фильмы, относящиеся только к нему, тоже исчезают.
-
Сделайте миграции:
python manage.py makemigrations python manage.py migrate
-
Откройте Django Admin.
-
Добавьте несколько жанров: «Драма», «Комедия», «Боевик».
-
Добавьте фильмы и укажите для каждого жанр.
Теперь при открытии списка фильмов вы увидите, что у каждого фильма есть свой жанр, а при удалении жанра — фильмы также удаляются (если CASCADE).
-
Ошибка
IntegrityError: NOT NULL constraint failed— Возникает, если при создании фильма вы не указали жанр, а полеgenreне допускаетnull.Решение: добавить
null=Trueиblank=True, если хотите, чтобы жанр был необязательным:genre = models.ForeignKey(Genre, on_delete=models.SET_NULL, null=True, blank=True)
-
Ошибка при удалении жанра — Если вы хотите, чтобы фильмы не удалялись, а просто оставались без жанра:
on_delete=models.SET_NULL
Теперь представим, что мы хотим хранить информацию об актёрах.
Один фильм может иметь несколько актёров, а один актёр может сниматься в нескольких фильмах.
Для этого создадим модели Actor и Movie.
class Actor(models.Model):
name = models.CharField(max_length=100)
birth_year = models.PositiveIntegerField(null=True, blank=True)
def __str__(self):
return self.name
class Movie(models.Model):
title = models.CharField(max_length=200)
year = models.PositiveIntegerField()
description = models.TextField()
genre = models.ForeignKey(Genre, on_delete=models.SET_NULL, null=True)
actors = models.ManyToManyField(Actor)
def __str__(self):
return self.titleЧто делает Django:
- автоматически создаёт промежуточную таблицу
movie_actors, где хранятся парыmovie_id↔actor_id; - не требует писать SQL для создания этой таблицы;
- позволяет легко добавлять, удалять и фильтровать связи.
-
Добавьте несколько актёров.
-
Откройте любой фильм — вы сможете выбрать сразу несколько актёров.
-
Сохраните изменения и убедитесь, что связь работает в обе стороны:
- у актёра видны все фильмы,
- у фильма — все актёры.
-
Ошибка:
ValueError: Cannot assign "..."— Возникает, если вы пытаетесь присвоить объект напрямую (например,movie.actors = actor) вместо использования методов.add()или.set().✅ Правильно:
movie.actors.add(actor)
Иногда нужно, чтобы каждой записи соответствовала ровно одна связанная запись.
Например, у фильма может быть одна детальная карточка, содержащая расширенную информацию — бюджет, длительность, режиссёра и т. д.
class MovieDetail(models.Model):
movie = models.OneToOneField(Movie, on_delete=models.CASCADE)
director = models.CharField(max_length=100)
duration = models.PositiveIntegerField(help_text="Продолжительность в минутах")
budget = models.PositiveIntegerField(null=True, blank=True)
def __str__(self):
return f"Детали фильма: {self.movie.title}"OneToOneFieldозначает, что каждой записиMovieможет соответствовать только одна записьMovieDetail.- При удалении фильма его детальная карточка также будет удалена.
- Создайте фильм.
- В админке создайте запись
MovieDetailи выберите этот фильм. - Попробуйте создать ещё одну запись
MovieDetailс тем же фильмом — Django не позволит это сделать, так как связь строго «один к одному».
| Тип связи | Пример | Поведение |
|---|---|---|
| ForeignKey | Movie → Genre |
Один жанр — много фильмов |
| ManyToMany | Movie ↔ Actor |
Один фильм — много актёров, один актёр — много фильмов |
| OneToOne | Movie → MovieDetail |
Один фильм — одна детальная запись |
Создайте и настройте следующие модели в приложении movies:
- Genre – жанры фильмов (название, слаг).
- Actor – актёры (имя, год рождения).
- Movie – фильмы (название, описание, год, рейтинг, жанр, актёры).
- MovieDetail – детальная информация (режиссёр, длительность, бюджет, связь 1:1 с фильмом).
Проверьте:
- при удалении жанра удаляются или обнуляются связанные фильмы (в зависимости от
on_delete); - один актёр может сниматься в нескольких фильмах;
- нельзя создать два объекта
MovieDetailдля одного фильма.
- Зачем нужны связи между таблицами?
- В чём разница между
ForeignKey,ManyToManyFieldиOneToOneField? - Что делает параметр
on_delete=models.CASCADE? - Как Django хранит данные для
ManyToManyFieldв базе данных? - Что произойдёт, если удалить жанр, на который ссылаются фильмы при
on_delete=models.SET_NULL? - Можно ли в одной модели иметь несколько
ForeignKeyна одну и ту же таблицу? - Как добавить актёра к фильму через Python-консоль?
- Почему
OneToOneFieldможно рассматривать как частный случайForeignKey? - Что будет, если попытаться создать две
MovieDetailдля одного фильма? - Как в админке Django отображаются поля
ManyToManyField?