Создание связи Many-to-Many в Django

Связь многие-ко-многим используется когда объект одной модели может быть связан с несколькими объектами другой модели, и наоборот. Это один из самых мощных типов связей в Django.

Базовый пример

 1from django.db import models
 2
 3class Author(models.Model):
 4    name = models.CharField(max_length=100)
 5    email = models.EmailField(unique=True)
 6    bio = models.TextField(blank=True)
 7    created_at = models.DateTimeField(auto_now_add=True)
 8
 9    def __str__(self):
10        return self.name
11
12    class Meta:
13        ordering = ['name']
14
15class Book(models.Model):
16    title = models.CharField(max_length=200)
17    description = models.TextField()
18    published_date = models.DateField()
19    authors = models.ManyToManyField(Author, related_name='books')
20    isbn = models.CharField(max_length=13, unique=True)
21
22    def __str__(self):
23        return self.title
24
25    class Meta:
26        ordering = ['-published_date']

Практические примеры использования

1. Создание и связывание объектов

 1# Создаем авторов
 2author1 = Author.objects.create(name='Джордж Мартин', email='george@example.com')
 3author2 = Author.objects.create(name='Дж.Р.Р. Толкин', email='tolkien@example.com')
 4author3 = Author.objects.create(name='Айзек Азимов', email='asimov@example.com')
 5
 6# Создаем книгу
 7book = Book.objects.create(
 8    title='Игра престолов',
 9    description='Эпическая сага о борьбе за власть',
10    published_date='1996-08-01',
11    isbn='9780553103540'
12)
13
14# Связываем книгу с авторами
15book.authors.add(author1)
16book.authors.add(author2, author3)  # Можно добавлять несколько сразу
17
18# Проверяем связи
19print(book.authors.all())  # QuerySet всех авторов
20print(author1.books.all())  # QuerySet всех книг автора (благодаря related_name)

2. Работа с промежуточной моделью (through)

 1class BookAuthor(models.Model):
 2    book = models.ForeignKey(Book, on_delete=models.CASCADE)
 3    author = models.ForeignKey(Author, on_delete=models.CASCADE)
 4    contribution_type = models.CharField(
 5        max_length=50,
 6        choices=[
 7            ('primary', 'Основной автор'),
 8            ('co_author', 'Соавтор'),
 9            ('editor', 'Редактор'),
10            ('translator', 'Переводчик'),
11        ]
12    )
13    contribution_percentage = models.IntegerField(
14        default=100,
15      help_text='Процент вклада в книгу'
16    )
17    created_at = models.DateTimeField(auto_now_add=True)
18
19    class Meta:
20        unique_together = ['book', 'author']
21        verbose_name = 'Автор книги'
22        verbose_name_plural = 'Авторы книги'
23
24# Обновляем модель Book для использования промежуточной модели
25class Book(models.Model):
26    title = models.CharField(max_length=200)
27    description = models.TextField()
28    published_date = models.DateField()
29    authors = models.ManyToManyField(
30        Author,
31        through=BookAuthor,
32        related_name='books'
33    )
34    isbn = models.CharField(max_length=13, unique=True)
35
36    def __str__(self):
37        return self.title
38
39# Теперь создаем связи через промежуточную модель
40book = Book.objects.create(
41    title='Новая книга',
42    description='Описание',
43    published_date='2024-01-01',
44    isbn='9780000000000'
45)
46
47BookAuthor.objects.create(
48    book=book,
49    author=author1,
50    contribution_type='primary',
51    contribution_percentage=60
52)
53
54BookAuthor.objects.create(
55    book=book,
56    author=author2,
57    contribution_type='co_author',
58    contribution_percentage=40
59)

3. Полезные методы для работы со связями

 1# Получение связанных объектов
 2book = Book.objects.get(title='Игра престолов')
 3
 4# Все авторы книги
 5authors = book.authors.all()
 6
 7# Авторы с дополнительными фильтрами
 8primary_authors = book.authors.filter(bookauthor__contribution_type='primary')
 9
10# Книги автора
11author = Author.objects.get(name='Джордж Мартин')
12author_books = author.books.all()
13
14# Книги автора с фильтрацией
15recent_books = author.books.filter(published_date__year__gte=2020)
16
17# Добавление и удаление связей
18new_author = Author.objects.create(name='Новый автор', email='new@example.com')
19book.authors.add(new_author)
20
21# Удаление связи
22book.authors.remove(author2)
23
24# Очистка всех связей
25book.authors.clear()
26
27# Проверка существования связи
28if book.authors.filter(id=author1.id).exists():
29    print(f"{author1.name} является автором {book.title}")
30
31# Количество связанных объектов
32author_count = book.authors.count()
33book_count = author.books.count()

4. Сложные запросы с Many-to-Many

 1from django.db.models import Count, Q
 2
 3# Книги с несколькими авторами
 4multi_author_books = Book.objects.annotate(
 5    author_count=Count('authors')
 6).filter(author_count__gt=1)
 7
 8# Авторы, написавшие более 5 книг
 9prolific_authors = Author.objects.annotate(
10    book_count=Count('books')
11).filter(book_count__gt=5).order_by('-book_count')
12
13# Книги определенного жанра (если есть поле genre)
14# Предположим, что у Book есть поле genre
15fantasy_books = Book.objects.filter(
16    Q(authors__name__icontains='Толкин') |
17    Q(authors__name__icontains='Мартин')
18).distinct()
19
20# Авторы, которые сотрудничали с определенным автором
21co_authors = Author.objects.filter(
22    books__authors__name='Джордж Мартин'
23).exclude(name='Джордж Мартин').distinct()

5. Админка для Many-to-Many

 1from django.contrib import admin
 2
 3@admin.register(Book)
 4class BookAdmin(admin.ModelAdmin):
 5    list_display = ['title', 'published_date', 'get_authors', 'isbn']
 6    list_filter = ['published_date', 'authors']
 7    search_fields = ['title', 'authors__name', 'isbn']
 8    filter_horizontal = ['authors']  # Удобный интерфейс для Many-to-Many
 9
10    def get_authors(self, obj):
11        return ", ".join([author.name for author in obj.authors.all()])
12    get_authors.short_description = 'Авторы'
13
14@admin.register(Author)
15class AuthorAdmin(admin.ModelAdmin):
16    list_display = ['name', 'email', 'book_count', 'created_at']
17    search_fields = ['name', 'email']
18    readonly_fields = ['created_at']
19
20    def book_count(self, obj):
21        return obj.books.count()
22    book_count.short_description = 'Количество книг'

Лучшие практики

  • related_name: Всегда указывай осмысленный related_name для удобства обратных запросов
  • through модель: Используй когда нужны дополнительные поля в связи
  • Индексы: Добавляй db_index=True для полей, по которым часто фильтруешь
  • unique_together: Используй в промежуточных моделях для предотвращения дублирования
  • Методы: Создавай удобные методы в моделях для работы со связями

Частые ошибки и их решения

Ошибка: "Cannot add the instance because it is not saved"

1# Неправильно
2book = Book(title='Новая книга')
3book.authors.add(author)  # Ошибка!
4
5# Правильно
6book = Book(title='Новая книга')
7book.save()
8book.authors.add(author)

Ошибка: N+1 запросы при итерации

 1# Неправильно - N+1 запросы
 2books = Book.objects.all()
 3for book in books:
 4    for author in book.authors.all():  # Новый запрос для каждой книги!
 5        print(f"{book.title} - {author.name}")
 6
 7# Правильно - используем prefetch_related
 8books = Book.objects.prefetch_related('authors').all()
 9for book in books:
10    for author in book.authors.all():  # Все авторы уже загружены
11        print(f"{book.title} - {author.name}")

FAQ

Q: Можно ли добавить дополнительные поля в связь?
A: Да, используй through parameter с промежуточной моделью. Это позволяет хранить метаданные о связи.

Q: Как избежать дублирования связей?
A: Используй unique_together в промежуточной модели или проверяй существование связи через exists().

Q: Когда использовать ManyToManyField vs ForeignKey?
A: ManyToManyField для связей "многие-ко-многим", ForeignKey для связей "один-ко-многим".

Q: Как оптимизировать запросы с Many-to-Many?
A: Используй select_related() для ForeignKey и prefetch_related() для ManyToManyField.

Q: Можно ли создать связь Many-to-Many с самим собой?
A: Да, используй symmetrical=False для несимметричных связей (например, "подписчики").