Связь "многие-ко-многим" в реляционных базах данных позволяет одному объекту быть связанным с несколькими другими объектами, и наоборот. Классические примеры из реальной жизни:
- Студенты и курсы: один студент может быть записан на несколько курсов, и курс может быть пройден несколькими студентами.
- Категории и продукты: продукт может принадлежать к разным категориям, а категория может содержать множество продуктов.
В Django такую связь можно реализовать с помощью поля ManyToManyField.
Реализация ManyToManyField в Django
1. Основной синтаксис
Предположим, мы разрабатываем приложение для книжного клуба, где каждая книга может быть выбрана несколькими читателями, а каждый читатель может прочитать множество книг.
Вот как это будет выглядеть в коде:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
def __str__(self):
return self.title
class Reader(models.Model):
name = models.CharField(max_length=100)
books = models.ManyToManyField(Book, related_name='readers')
def __str__(self):
return self.name
Что здесь происходит?
- Поле
booksв моделиReaderустанавливает связь "многие-ко-многим" с модельюBook. - Аргумент
related_name='readers'позволяет получить список всех читателей, связанный с определённой книгой, используяbook.readers.all().
2. Как это хранится в базе данных?
За кулисами Django создаёт промежуточную таблицу. Её название будет состоять из _ между именами двух моделей, например: appname_reader_books. Эта таблица связывает id модели Book с id модели Reader.
| id | reader_id | book_id |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
Примеры использования ManyToManyField
Создание связанных объектов
# Создаём объекты
book1 = Book.objects.create(title="Война и мир", author="Лев Толстой")
book2 = Book.objects.create(title="Преступление и наказание", author="Фёдор Достоевский")
reader1 = Reader.objects.create(name="Иван Иванов")
reader2 = Reader.objects.create(name="Мария Петрова")
# Связываем читателей с книгами
reader1.books.add(book1, book2) # Иван читает две книги
reader2.books.add(book1) # Мария читает только одну книгу
Доступ к связанным объектам
- Узнать все книги, которые читает Иван:
ivan_books = reader1.books.all() # [<Book: Война и мир>, <Book: Преступление и наказание>]
- Узнать всех читателей, которые выбрали "Война и мир":
war_and_peace_readers = book1.readers.all() # [<Reader: Иван Иванов>, <Reader: Мария Петрова>]
Удаление связей
# Иван передумал читать "Преступление и наказание"
reader1.books.remove(book2)
# Убедимся, что связь удалена
reader1.books.all() # [<Book: Война и мир>]
Для удаления всех связей сразу используйте clear():
reader1.books.clear()
reader1.books.all() # []
Промежуточная модель для дополнительной информации
Иногда вам нужно хранить дополнительную информацию о связи. Например, дата, когда читатель начал читать книгу. Для этого создаётся промежуточная модель.
- Создание промежуточной модели
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
def __str__(self):
return self.title
class Reader(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Reading(models.Model):
reader = models.ForeignKey(Reader, on_delete=models.CASCADE)
book = models.ForeignKey(Book, on_delete=models.CASCADE)
started_at = models.DateField()
- Работа с промежуточной моделью
Теперь мы взаимодействуем с отношениями напрямую через Reading:
# Иван начинает читать "Война и мир"
reading = Reading.objects.create(reader=reader1, book=book1, started_at="2023-10-01")
# Получим всех читателей "Войны и мира"
book_readers = Reading.objects.filter(book=book1).select_related('reader')
for reading in book_readers:
print(reading.reader.name, reading.started_at)
Особенности и типичные ошибки
Ошибки с промежуточными моделями. Если вы определяете промежуточную модель, то использовать поле
ManyToManyFieldуже нельзя. Связь обрабатывается напрямую через промежуточную модель.Ошибка при отсутствии
related_name. Если забудете указатьrelated_name, обратные связи будут по умолчанию иметь имена вродеreader_set, что может показаться некрасивым. Всегда желательно задавать осмысленныеrelated_name.Проблемы с оптимизацией запросов. Использование
prefetch_related()для связей "многие-ко-многим" особенно важно, если вам нужно получить связанные данные в одном запросе.
Оптимизация Many-to-Many с prefetch_related
Предположим, мы хотим загрузить всех читателей и их книги:
readers = Reader.objects.prefetch_related('books')
for reader in readers:
print(reader.name, [book.title for book in reader.books.all()])
prefetch_related предотвращает выполнение отдельного SQL-запроса для каждой книги читателя, группируя их в один запрос.
Практическое задание
Создайте модели для кинотеатра:
Movieс полямиtitleиrelease_year.Actorс полемname.- Связь между фильмами и актёрами должна быть "многие-ко-многим".
- Создайте несколько фильмов и актёров.
- Свяжите фильмы с актёрами.
- Напишите запросы для получения:
- Фильмов, в которых снялся конкретный актёр.
- Актёров, участвовавших в конкретном фильме.
- Всех фильмов вместе с их актёрским составом (с использованием
prefetch_related).
И помните, связь "многие-ко-многим" — это как команда разработчиков в проекте: каждый разработчик может участвовать в нескольких проектах, и каждый проект не обходится без нескольких разработчиков! 😉
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ