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).

І пам'ятайте, зв'язок "багато-до-багатьох" — це як команда розробників у проєкті: кожен розробник може брати участь у кількох проєктах, і кожен проєкт не обходиться без кількох розробників! 😉

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ