JavaRush /Курсы /Модуль 3: Django /Связь ManyToMany и её особенности в Django

Связь ManyToMany и её особенности в Django

Модуль 3: Django
9 уровень , 3 лекция
Открыта

Связь "многие-ко-многим" в реляционных базах данных позволяет одному объекту быть связанным с несколькими другими объектами, и наоборот. Классические примеры из реальной жизни:

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

В 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)  # Мария читает только одну книгу

Доступ к связанным объектам

  1. Узнать все книги, которые читает Иван:
ivan_books = reader1.books.all()  # [<Book: Война и мир>, <Book: Преступление и наказание>]
  1. Узнать всех читателей, которые выбрали "Война и мир":
war_and_peace_readers = book1.readers.all()  # [<Reader: Иван Иванов>, <Reader: Мария Петрова>]

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

# Иван передумал читать "Преступление и наказание"
reader1.books.remove(book2)

# Убедимся, что связь удалена
reader1.books.all()  # [<Book: Война и мир>]

Для удаления всех связей сразу используйте clear():

reader1.books.clear()
reader1.books.all()  # []

Промежуточная модель для дополнительной информации

Иногда вам нужно хранить дополнительную информацию о связи. Например, дата, когда читатель начал читать книгу. Для этого создаётся промежуточная модель.

  1. Создание промежуточной модели
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()
  1. Работа с промежуточной моделью

Теперь мы взаимодействуем с отношениями напрямую через 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.
  • Связь между фильмами и актёрами должна быть "многие-ко-многим".
  1. Создайте несколько фильмов и актёров.
  2. Свяжите фильмы с актёрами.
  3. Напишите запросы для получения:
    • Фильмов, в которых снялся конкретный актёр.
    • Актёров, участвовавших в конкретном фильме.
    • Всех фильмов вместе с их актёрским составом (с использованием prefetch_related).

И помните, связь "многие-ко-многим" — это как команда разработчиков в проекте: каждый разработчик может участвовать в нескольких проектах, и каждый проект не обходится без нескольких разработчиков! 😉

1
Задача
Модуль 3: Django, 9 уровень, 3 лекция
Недоступна
Создание связи ManyToMany
Создание связи ManyToMany
1
Задача
Модуль 3: Django, 9 уровень, 3 лекция
Недоступна
Работа с ManyToMany связью
Работа с ManyToMany связью
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Максим Уровень 69
13 ноября 2025
в первой задаче необходимо добавить в admin.py from .models import Teacher, Subject # Регистрация модели Teacher в административной панели Django. @admin.register(Teacher) class TeacherAdmin(admin.ModelAdmin): list_display = ('name',) search_fields = ('name',) # Добавляем возможность поиска по имени учителя # Регистрация модели Subject в административной панели Django. @admin.register(Subject) class SubjectAdmin(admin.ModelAdmin): list_display = ('name',) search_fields = ('name',) # Добавляем возможность поиска по названию предмета