Зв'язок "багато-до-багатьох" у реляційних базах даних дозволяє одному об'єкту бути пов'язаним з кількома іншими об'єктами, і навпаки. Класичні приклади з реального життя:
- Студенти та курси: один студент може бути записаний на кілька курсів, і курс може бути пройдений кількома студентами.
- Категорії та продукти: продукт може належати до різних категорій, а категорія може містити багато продуктів.
У 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).
І пам'ятайте, зв'язок "багато-до-багатьох" — це як команда розробників у проєкті: кожен розробник може брати участь у кількох проєктах, і кожен проєкт не обходиться без кількох розробників! 😉
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ