Связь многие ко многим описывает ситуацию, когда объект первой модели может одновременно ассоциироваться с несколькими объектами второй модели. И наоборот, один объект второй модели может также одновременно быть ассоциирован с несколькими объектами первой модели. Например, один студент может посещать несколько курсов, а один курс могут посещать несколько студентов.
Для создания отношения многие ко многим применяется тип 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())