Skip to content

Latest commit

 

History

History
276 lines (182 loc) · 9.52 KB

File metadata and controls

276 lines (182 loc) · 9.52 KB

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

В предыдущих уроках мы уже научились создавать связи "один ко многим" (ForeignKey) и "один к одному" (OneToOneField).

Теперь пришло время познакомиться с ещё одним важным типом связей — "многие ко многим" (Many-to-Many).

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


Пример из проекта CinemaHub

В проекте CinemaHub у нас есть фильмы, актёры, режиссёры, жанры и пользователи.

Посмотрим на один из естественных примеров связи "многие ко многим" — фильм и жанры:

  • Один фильм может относиться сразу к нескольким жанрам. Например, «Матрица» — это и фантастика, и боевик.
  • Один жанр может относиться ко множеству фильмов. Например, жанр боевик подходит и для «Матрицы», и для «Джона Уика».

Такую ситуацию невозможно корректно описать через ForeignKey, ведь тогда пришлось бы дублировать записи.


Реализация связи Many-to-Many

Шаг 1. Создаём модель жанров

В файле movies/models.py создадим модель Genre:

from django.db import models

class Genre(models.Model):
    name = models.CharField(max_length=100, db_index=True, unique=True)
    slug = models.SlugField(max_length=255, unique=True, db_index=True)

    def __str__(self):
        return self.name

Здесь всё просто:

  • name — название жанра (например, "Комедия").
  • slug — удобное текстовое представление в URL.
  • db_index=True — ускоряет поиск по этим полям.

Шаг 2. Добавляем связь в модель фильмов

Теперь обновим модель Movie, чтобы добавить поле genres:

class Movie(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255, unique=True)
    release_year = models.PositiveIntegerField()
    description = models.TextField(blank=True)
    genres = models.ManyToManyField('Genre', blank=True, related_name='movies')

    def __str__(self):
        return self.title

Разберём ключевые детали:

  • ManyToManyField('Genre') — говорит Django, что один фильм может иметь несколько жанров.
  • 'Genre' записан в кавычках, потому что класс Genre объявлен ниже (или в другом файле).
  • blank=True позволяет оставить список жанров пустым.
  • related_name='movies' — создаёт обратную связь, чтобы можно было обратиться от жанра к фильмам (например: genre.movies.all()).

Шаг 3. Создаём и применяем миграции

После изменений обязательно нужно создать и применить миграции:

python manage.py makemigrations
python manage.py migrate

Теперь Django создаст три таблицы:

  1. movies_movie — таблица с фильмами.
  2. movies_genre — таблица с жанрами.
  3. movies_movie_genresвспомогательная таблица, где хранятся связи между фильмами и жанрами.

Как работает связь Many-to-Many

Всё, что делает Django — это создаёт промежуточную таблицу, которая хранит пары ID:

id movie_id genre_id
1 3 1
2 3 4
3 5 1

Эта таблица позволяет быстро узнать:

  • Какие жанры принадлежат фильму с ID=3.
  • Какие фильмы относятся к жанру с ID=1.

Работа с Many-to-Many через Django ORM

Теперь перейдём в интерактивную оболочку Django:

python manage.py shell

Создание жанров

from movies.models import Movie, Genre

g1 = Genre.objects.create(name='Боевик', slug='action')
g2 = Genre.objects.create(name='Фантастика', slug='sci-fi')
g3 = Genre.objects.create(name='Драма', slug='drama')

Создание фильма

m = Movie.objects.create(title='Матрица', slug='matrix', release_year=1999)

Привязка жанров к фильму

m.genres.set([g1, g2])  # Устанавливаем несколько жанров

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

m.genres.all()
# <QuerySet [<Genre: Боевик>, <Genre: Фантастика>]>

Получение фильмов по жанру

g1.movies.all()
# <QuerySet [<Movie: Матрица>]>

Добавление ещё одного жанра

g4 = Genre.objects.create(name='Триллер', slug='thriller')
m.genres.add(g4)
m.genres.all()
# <QuerySet [<Genre: Боевик>, <Genre: Фантастика>, <Genre: Триллер>]>

Удаление связи

m.genres.remove(g1)
m.genres.all()
# <QuerySet [<Genre: Фантастика>, <Genre: Триллер>]>

Очистка всех связей

m.genres.clear()
m.genres.all()
# <QuerySet []>

Частая ошибка

Многие пытаются сразу создать объект с тегами или жанрами:

# ❌ Так не сработает:
Movie.objects.create(title='Интерстеллар', slug='interstellar', genres=[g2, g3])

ManyToManyField не принимает список жанров при создании! Сначала создайте объект, потом привяжите жанры:

# ✅ Правильный способ:
m2 = Movie.objects.create(title='Интерстеллар', slug='interstellar', release_year=2014)
m2.genres.set([g2, g3])

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

  1. Откройте панель администратора Django (/admin).
  2. Добавьте несколько жанров вручную.
  3. Создайте фильм и отметьте нужные жанры в поле "Жанры".
  4. Сохраните и убедитесь, что жанры отображаются корректно.

Практическая часть

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

Задание 1

Создайте три жанра: Комедия, Фэнтези, Мультфильм.

Затем создайте фильм «Шрек» и привяжите к нему все три жанра. Выведите список всех жанров, связанных с этим фильмом.


Задание 2

Создайте фильм «Начало» (2010). Присвойте ему жанры Фантастика и Триллер. Удалите жанр Триллер, а затем добавьте Драма. Проверьте результат через ORM.


Задание 3

Выведите все фильмы, принадлежащие жанру Фантастика. Попробуйте добавить ещё один фильм к этому жанру и убедитесь, что запрос возвращает оба.


Задание 4

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


Вопросы

  1. Что представляет собой связь "многие ко многим" в базах данных?
  2. Чем ManyToManyField отличается от ForeignKey?
  3. Для чего нужна промежуточная таблица в Many-to-Many?
  4. Можно ли передать список жанров при создании фильма через create()?
  5. Как удалить один конкретный жанр у фильма?
  6. Как получить все фильмы, относящиеся к определённому жанру?
  7. Что делает метод .clear() у поля ManyToMany?
  8. Что произойдёт, если удалить жанр из таблицы Genre?
  9. Можно ли использовать параметр on_delete в ManyToManyField?
  10. Почему важно добавлять related_name?

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