Skip to content

Latest commit

 

History

History
262 lines (172 loc) · 12.7 KB

File metadata and controls

262 lines (172 loc) · 12.7 KB

Модуль 4. Урок 21. Типы связей между моделями в Django: ForeignKey, ManyToManyField, OneToOneField

🎬 Введение: зачем вообще нужны связи между таблицами?

Представим, что мы создаём проект Cinemahub — сайт о фильмах и сериалах.

У нас уже есть таблица с фильмами, где хранятся их названия, описание, год выпуска и рейтинг. Всё работает, пока данных немного.

Но что если мы захотим добавить информацию о жанрах, актёрах, режиссёрах, странах, категориях (фильмы / сериалы) и т. д.?

Наивный вариант — добавить все эти данные в таблицу Movie. Например, сделать поля:

genre = "драма, триллер"
actors = "Киану Ривз, Лоуренс Фишбёрн"
country = "США"

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

  • Дублирование данных: имена актёров, жанры и страны будут постоянно повторяться.
  • Сложность изменений: если актёр сменил имя, придётся менять его во всех фильмах.
  • Падение производительности: фильтрация по текстовым полям всегда медленнее, чем по числовым ключам.

Чтобы избежать этого, базы данных используют связи между таблицами — способ разложить данные по отдельным таблицам и связать их логически.

Такой подход называется нормализацией данных.


Зачем нам нормализация?

  • уменьшает избыточность данных;
  • упрощает редактирование (изменения в одной таблице автоматически влияют на связанные записи);
  • ускоряет запросы и фильтрацию.

Типы связей в Django ORM

Django поддерживает три основных типа связей между моделями:

Тип связи Django-поле Описание
Многие к одному ForeignKey Один объект может принадлежать многим другим (пример: фильм → категория).
Многие ко многим ManyToManyField Обе стороны могут иметь множество связей (пример: актёр ↔ фильм).
Один к одному OneToOneField Каждая запись связана только с одной другой (пример: фильм ↔ детальная карточка).

ForeignKey — связь «многие к одному»

Один из самых частых типов связи.

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

Создадим две модели: 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 означает: если жанр удалить, то все связанные фильмы тоже будут удалены.

💡 Это логично: если мы удаляем жанр «Драма», фильмы, относящиеся только к нему, тоже исчезают.


Проверим результат

  1. Сделайте миграции:

    python manage.py makemigrations
    python manage.py migrate
  2. Откройте Django Admin.

  3. Добавьте несколько жанров: «Драма», «Комедия», «Боевик».

  4. Добавьте фильмы и укажите для каждого жанр.

Теперь при открытии списка фильмов вы увидите, что у каждого фильма есть свой жанр, а при удалении жанра — фильмы также удаляются (если CASCADE).


Возможные ошибки

  1. Ошибка IntegrityError: NOT NULL constraint failed — Возникает, если при создании фильма вы не указали жанр, а поле genre не допускает null.

    Решение: добавить null=True и blank=True, если хотите, чтобы жанр был необязательным:

    genre = models.ForeignKey(Genre, on_delete=models.SET_NULL, null=True, blank=True)
  2. Ошибка при удалении жанра — Если вы хотите, чтобы фильмы не удалялись, а просто оставались без жанра:

    on_delete=models.SET_NULL

ManyToManyField — связь «многие ко многим»

Теперь представим, что мы хотим хранить информацию об актёрах.

Один фильм может иметь несколько актёров, а один актёр может сниматься в нескольких фильмах.

Для этого создадим модели 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_idactor_id;
  • не требует писать SQL для создания этой таблицы;
  • позволяет легко добавлять, удалять и фильтровать связи.

Проверка в админке

  1. Добавьте несколько актёров.

  2. Откройте любой фильм — вы сможете выбрать сразу несколько актёров.

  3. Сохраните изменения и убедитесь, что связь работает в обе стороны:

    • у актёра видны все фильмы,
    • у фильма — все актёры.

Возможные ошибки

  • Ошибка: ValueError: Cannot assign "..." — Возникает, если вы пытаетесь присвоить объект напрямую (например, movie.actors = actor) вместо использования методов .add() или .set().

    ✅ Правильно:

    movie.actors.add(actor)

OneToOneField — связь «один к одному»

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

Например, у фильма может быть одна детальная карточка, содержащая расширенную информацию — бюджет, длительность, режиссёра и т. д.

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.
  • При удалении фильма его детальная карточка также будет удалена.

Проверка работы OneToOneField

  1. Создайте фильм.
  2. В админке создайте запись MovieDetail и выберите этот фильм.
  3. Попробуйте создать ещё одну запись MovieDetail с тем же фильмом — Django не позволит это сделать, так как связь строго «один к одному».

Визуальное представление связей

Тип связи Пример Поведение
ForeignKey Movie → Genre Один жанр — много фильмов
ManyToMany Movie ↔ Actor Один фильм — много актёров, один актёр — много фильмов
OneToOne Movie → MovieDetail Один фильм — одна детальная запись

Практическая работа

Создайте и настройте следующие модели в приложении movies:

  1. Genre – жанры фильмов (название, слаг).
  2. Actor – актёры (имя, год рождения).
  3. Movie – фильмы (название, описание, год, рейтинг, жанр, актёры).
  4. MovieDetail – детальная информация (режиссёр, длительность, бюджет, связь 1:1 с фильмом).

Проверьте:

  • при удалении жанра удаляются или обнуляются связанные фильмы (в зависимости от on_delete);
  • один актёр может сниматься в нескольких фильмах;
  • нельзя создать два объекта MovieDetail для одного фильма.

Вопросы

  1. Зачем нужны связи между таблицами?
  2. В чём разница между ForeignKey, ManyToManyField и OneToOneField?
  3. Что делает параметр on_delete=models.CASCADE?
  4. Как Django хранит данные для ManyToManyField в базе данных?
  5. Что произойдёт, если удалить жанр, на который ссылаются фильмы при on_delete=models.SET_NULL?
  6. Можно ли в одной модели иметь несколько ForeignKey на одну и ту же таблицу?
  7. Как добавить актёра к фильму через Python-консоль?
  8. Почему OneToOneField можно рассматривать как частный случай ForeignKey?
  9. Что будет, если попытаться создать две MovieDetail для одного фильма?
  10. Как в админке Django отображаются поля ManyToManyField?

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