Создание связи 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"
Ошибка: 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 для несимметричных связей (например, "подписчики").