В предыдущих уроках мы уже научились создавать связи "один ко многим" (ForeignKey) и "один к одному" (OneToOneField).
Теперь пришло время познакомиться с ещё одним важным типом связей — "многие ко многим" (Many-to-Many).
Эта связь используется, когда одна запись может быть связана с несколькими другими, и наоборот.
В проекте CinemaHub у нас есть фильмы, актёры, режиссёры, жанры и пользователи.
Посмотрим на один из естественных примеров связи "многие ко многим" — фильм и жанры:
- Один фильм может относиться сразу к нескольким жанрам. Например, «Матрица» — это и фантастика, и боевик.
- Один жанр может относиться ко множеству фильмов. Например, жанр боевик подходит и для «Матрицы», и для «Джона Уика».
Такую ситуацию невозможно корректно описать через ForeignKey, ведь тогда пришлось бы дублировать записи.
В файле 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— ускоряет поиск по этим полям.
Теперь обновим модель 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()).
После изменений обязательно нужно создать и применить миграции:
python manage.py makemigrations
python manage.py migrateТеперь Django создаст три таблицы:
movies_movie— таблица с фильмами.movies_genre— таблица с жанрами.movies_movie_genres— вспомогательная таблица, где хранятся связи между фильмами и жанрами.
Всё, что делает Django — это создаёт промежуточную таблицу, которая хранит пары ID:
| id | movie_id | genre_id |
|---|---|---|
| 1 | 3 | 1 |
| 2 | 3 | 4 |
| 3 | 5 | 1 |
Эта таблица позволяет быстро узнать:
- Какие жанры принадлежат фильму с ID=3.
- Какие фильмы относятся к жанру с ID=1.
Теперь перейдём в интерактивную оболочку Django:
python manage.py shellfrom 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])- Откройте панель администратора Django (
/admin). - Добавьте несколько жанров вручную.
- Создайте фильм и отметьте нужные жанры в поле "Жанры".
- Сохраните и убедитесь, что жанры отображаются корректно.
Попробуйте выполнить эти задания, используя только то, что изучили в уроке.
Создайте три жанра:
Комедия, Фэнтези, Мультфильм.
Затем создайте фильм «Шрек» и привяжите к нему все три жанра. Выведите список всех жанров, связанных с этим фильмом.
Создайте фильм «Начало» (2010).
Присвойте ему жанры Фантастика и Триллер.
Удалите жанр Триллер, а затем добавьте Драма.
Проверьте результат через ORM.
Выведите все фильмы, принадлежащие жанру Фантастика.
Попробуйте добавить ещё один фильм к этому жанру и убедитесь, что запрос возвращает оба.
Создайте жанр Ужасы, но не привязывайте его ни к одному фильму.
Выведите список всех фильмов, у которых жанр Ужасы. Что вы получите?
- Что представляет собой связь "многие ко многим" в базах данных?
- Чем
ManyToManyFieldотличается отForeignKey? - Для чего нужна промежуточная таблица в Many-to-Many?
- Можно ли передать список жанров при создании фильма через
create()? - Как удалить один конкретный жанр у фильма?
- Как получить все фильмы, относящиеся к определённому жанру?
- Что делает метод
.clear()у поля ManyToMany? - Что произойдёт, если удалить жанр из таблицы
Genre? - Можно ли использовать параметр
on_deleteв ManyToManyField? - Почему важно добавлять
related_name?