Отношение многие ко многим (Many to Many)

Последнее обновление: 22.08.2022

Связь многие ко многим описывает ситуацию, когда объект первой модели может одновременно ассоциироваться с несколькими объектами второй модели. И наоборот, один объект второй модели может также одновременно быть ассоциирован с несколькими объектами первой модели. Например, один студент может посещать несколько курсов, а один курс могут посещать несколько студентов.

Для создания отношения многие ко многим применяется тип ManyToManyField.

from django.db import models

class Course(models.Model):
    name = models.CharField(max_length=30)

class Student(models.Model):
    name = models.CharField(max_length=30)
    courses = models.ManyToManyField(Course)

В конструктор models.ManyToManyField передается сущность, с которой устанавливается отношение многие ко многим. В результате будет создаваться промежуточная таблица, через которую собственно и будет осуществляться связь.

В результате миграции в базе данных SQLite будут создаваться следующие таблицы:

CREATE TABLE "hello_course" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL)
CREATE TABLE "hello_student" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL)
CREATE TABLE "hello_student_courses" (
	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
	"student_id" bigint NOT NULL REFERENCES "hello_student" ("id") DEFERRABLE INITIALLY DEFERRED, 
	"course_id" bigint NOT NULL REFERENCES "hello_course" ("id") DEFERRABLE INITIALLY DEFERRED
)

В данном случае "hello_student_courses" выступает в качестве связующей таблицы. Она называется по шаблону имя_таблицы + имя_связующего_поля_из_таблицы.

Операции с моделями

Через свойство courses в модели Student мы можем получать связанные со студентом курсы и управлять ими.

# создадим студента
tom = Student.objects.create(name="Tom")

# создадим один курс и добавим его в список курсов Тома
tom.courses.create(name="Algebra")

# получим все курсы студента
courses = Student.objects.get(name="Tom").courses.all()

# получаем всех студентов, которые посещают курс Алгебра
students = Student.objects.filter(courses__name="Algebra")

Стоит отметить последний случай, где производится фильтрация студентов по посещаемому курсу. Для передачи в метод filter названия курса используется параметр, название которого начинается с названия свойства, через которое идет связь со второй моделью. И далее через два знака подчеркивания указывается имя свойства второй модели, например, courses__name или courses__id.

В данном случае мы можем получить информацию о курсах студента через свойство courses, которое определено в модели Student. Однако что если мы хотим получить информацию о студентах по определенному курсу? В этом случае нам надо использовать синтаксис _set.

# создадим курс
python = Course.objects.create(name="Python")

# создаем студента и добавляем его на курс
python.student_set.create(name="Bob")

# отдельно создаем студента и добавляем его на курс
sam = Student(name="Sam")
sam.save()
python.student_set.add(sam)

# получим всех студентов курса
students = python.student_set.all()

# получим количество студентов по курсу
number = python.student_set.count()

# удялем с курса одного студента
python.student_set.remove(sam)

# удаляем всех студентов с курса
python.student_set.clear()

Определение промежуточной сущности

Стоит учитывать, что вышеуказанная организация моделей не всегда может подойти. Например, в данном случае создается промежуточная таблица, которая хранит id студента и id курса. Если нам надо в промежуточной таблице хранить еще какие-либо данные, например, дату зачисления студента на курс, его оценку и т.д., то такая конфигурация не подойдет. И в этом случае есть несколько возможных вариантов. Самый просто из них - создать промежуточную модель вручную, которая будет хранить все дополнительные атрибуты (например, оценку студента по курсу, дату зачисления) и которая будет связана отношением один ко многим с обеими моделями.

Но Django также предоставляет другой способ, при котором обе значимые модели остаются связанными отношением многие-ко-многим, и при этом также определяется вспомогательная промежуточная модель. Так, изменим выше определенные модели Course и Student следующим образом:

from django.db import models

class Course(models.Model):
    name = models.CharField(max_length=30)

class Student(models.Model):
    name = models.CharField(max_length=30)
    courses = models.ManyToManyField(Course, through="Enrollment")

class Enrollment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    date = models.DateField()   # дата поступления
    mark = models.IntegerField(max_value=5)  # полученный балл

Модели Student и Course остаются связанными отношением многие-ко-многим, однако теперь мы явно определяем промежуточную модель - Enrollment, которая связана отношением один-ко-многим с обеими моделями и при этом также определяет два дополнительных поля. Для связи с промежуточной моделью в конструктор ForeignKey передается параметр through, который указывает на название промежуточной таблицы, создаваемой для промежуточной моедли

В случае с SQLite мы получим следующие таблицы:

CREATE TABLE "hello_course" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL)
CREATE TABLE "hello_student" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL)
CREATE TABLE "hello_enrollment" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "date" date NOT NULL, 
    "mark" integer NOT NULL, 
    "course_id" bigint NOT NULL REFERENCES "hello_course" ("id") DEFERRABLE INITIALLY DEFERRED, 
    "student_id" bigint NOT NULL REFERENCES "hello_student" ("id") DEFERRABLE INITIALLY DEFERRED
)

Использование промежуточной модели:

# создадим курс
    python = Course.objects.create(name="Python")

    # создадим двух студентов
    tom = Student.objects.create(name="Tom")
    sam = Student.objects.create(name="Sam")

    # создаем поступления студентов на курс
    tom_python = Enrollment(student = tom, course= python,
                    date=date.today(), mark = 5)
    sam_python = Enrollment(student = sam, course= python,
                    date=date.today(), mark = 4)
    # сохраняем поступления в бд
    tom_python.save()
    sam_python.save()

    # получаем все курсы у Toma
    tom_courses = tom.courses.all()
    print(tom_courses[0].name)      # python
    
    # получим всех студентов курса по python
    python_students = python.student_set.all()
    print(python_students[0].name)      # Tom

Подобным образом мы можем вызывать у объектов моделей методы add(), create() и set() для установки отношения с объектов другой модели. Но при этом необходимо передать через параметр through_defaults значения для полей промежуточной модели, которые требуют наличия значения.

# создадим курсы
django = Course.objects.create(name="Django")
python = Course.objects.create(name="Python")
java = Course.objects.create(name="Java")

# создадим студента
bob = Student.objects.create(name="Bob")

# добавляем курс для студента bob
bob.courses.add(django, through_defaults={"date": date.today(), "mark": 5})
# создаем курс для студента bob
bob.courses.create(name="C++", through_defaults={"date": date.today(), "mark": 4})
    
# получаем все курсы Boba
print(bob.courses.all().values_list()) # <QuerySet [(11, 'Django'), (14, 'C++')]>

# переустанавливаем курсы для студента bob
bob.courses.set([python, java], through_defaults={"date": date.today(), "mark": 4})
    
# снова получаем все курсы Boba
print(bob.courses.all().values_list()) # <QuerySet [(12, 'Python'), (13, 'Java')]>

Также с помощью методов remove() и clear() можно удалить связь одной модели с другой:

# создадим курсы
django = Course.objects.create(name="Django")
python = Course.objects.create(name="Python")

# создадим студента
tim = Student.objects.create(name="Tim")

# устанавливаем курсы для студента Tim
tim.courses.set([python, django], through_defaults={"date": date.today(), "mark": 4})
    
print(tim.courses.all().values_list()) # <QuerySet [(16, 'Python'), (15, 'Django')]>
    
# удаляем один курс
tim.courses.remove(django)
print(tim.courses.all().values_list()) # <QuerySet [(16, 'Python')]>

# удаляем все курсы
tim.courses.clear()
print(tim.courses.all().values_list()) # <QuerySet []>

Также мы можем применять фильтрацию по полям всех трех моделей. Например, найдем всех студентов, у которых курс - Python, а оценка - 4 или ниже:

# фильтрация
students = Student.objects.filter(
    courses__name="Python",
    enrollment__mark__lte=4)

print(students.values_list())
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850