Организация связей между таблицами "многие-ко-многим"
Связь «многие-ко-многим» - это связь, при которой множественным записям из одной таблицы (А) могут соответствовать множественные записи из другой таблицы (В).
Примером такой связи может служить учебное заведение, где преподаватели обучают учащихся. В большинстве учебных заведений (школа, университет) каждый преподаватель обучает многих учащихся, а каждый учащийся может обучаться у нескольких преподавателей.
Еще один пример- это книги и авторы книг. У одной книги может быть несколько авторов, в то же время у одного автора может быть несколько книг.
Связь «многие-ко-многим» создается с помощью трех таблиц: две из них (А и В) - "источники" и одна таблица - соединительная. Первичный ключ соединительной таблицы (А-В) - составной. Она состоит из двух полей: двух внешних ключей, которые ссылаются на первичные ключи таблиц А и В. Все первичные ключи должны быть уникальными. Это подразумевает и то, что комбинация полей А и В должна быть уникальной в таблице А-В.
Еще одним примером такой связи являются номера гостиницы и её гости. Между таблицами «Гости» и «Комнаты» (или номера) существует связь «многие-ко-многим»: одна комната может быть заказана многими гостями в течение определенного времени, и в течение этого же промежутка времени гость может заказать в гостинице разные комнаты.
Однако соединительная таблица в таком случае может не являться классической соединительной таблицей, состоящей только из двух внешних ключей. Она может быть отдельной сущностью с дополнительными полями, имеющей связи с двумя другими сущностями (при этом уникальность ключей должна соблюдаться).
Вследствие природы отношения «многие-ко-многим» совершенно неважно, какая из таблиц является родительской, а какая - дочерней, потому что по своей сути такой тип отношений является симметричным.
Для представления отношения «многие-ко-многим» Django самостоятельно создает промежуточную связывающую таблицу. По умолчанию имя этой таблицы образуется из имен двух соединяемых таблиц.
Рассмотрим, как можно связать две таблицы в БД через связанные модели на примере: «много учебных курсов - много студентов».
Для создания отношения «многие-ко-многим» применяется тип связи ManyТoManyField.
Создадим новое приложение в нашем проекте:
python manage.py startapp manytomany
Итак, рассмотрим в файле models.py следующий код:
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)
В этом примере модель Course представляет собой учебные курсы, а модель Student - студентов.
Здесь нет родительской и дочерней таблицы, они равнозначны.
- модель таблицы данных о курсах - с именем
Courseи двумя полями:'id'и'name'; - модель таблицы данных о студентах - с именем
Studentи двумя полями:'id'и'name'. При этом указано, что между таблицами имеется связующая таблица, которая обеспечивает связь «многие-ко-многим», и к этим связям можно обращаться через свойствоcourses.
Новая сущность courses, устанавливающая отношение «многие-ко-многим», создается с использованием конструктора models.ManyТoManyField. В результате генерируется промежуточная таблица, через которую, собственно, и будет осуществляться связь. Зарегистрируем приложение в INSTALLED_APPS и выполним миграции.
python manage.py makemigrations
python manage.py migrate
Посмотрим на нашу БД:
В результате миграции на основе моделей Course и Student в базе данных SQLite автоматически будут созданы уже не две, а три таблицы: manytomany_course, manytomany_student и manytomany_student_courses.
CREATE TABLE "manytomany_course" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL)
CREATE TABLE "manytomany_student" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL)
CREATE TABLE "manytomany_student_courses" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"student_id" bigint NOT NULL REFERENCES "manytomany_student" ("id") DEFERRABLE INITIALLY DEFERRED,
"course_id" bigint NOT NULL REFERENCES "manytomany_course" ("id") DEFERRABLE INITIALLY DEFERRED
)
В нашем случае таблица с именем manytomany_student_courses выступает в качестве связующей таблицы. Ей автоматически присвоено имя по шаблону:
имя_приложения + имя_таблицы + имя_связующего_поля_из_таблицы
А в самой связующей таблице имеются всего два поля с ключами из двух связанных таблиц: student_id и course_id.
Через свойство courses в модели Student мы можем получать связанные со студентом курсы и управлять ими.
Запустим шелл и импортируем модели:
python manage.py shell
from manytomany.models import *
Рассмотрим следующий код:
# создадим студента с именем Виктор
stud_viktor = Student.objects.create(name="Виктор")
# создадим один курс и добавим в него Виктора
stud_viktor.courses.create(name="Django")
# получим все курсы студента Виктора
all_courses = Student.objects.get(name="Виктор").courses.all()
# all_courses будет содержать <QuerySet [<Course: Course object (1)>]>
# получаем всех студентов, которые посещают курс Django
all_student = Student.objects.filter(courses__name="Django")
# all_courses будет содержать <QuerySet [<Course: Course object (1)>]>
Стоит обратить внимание на последнюю строку кода, где производится выборка студентов по посещаемому курсу. Для передачи в метод filter() имени курса используется параметр, название которого начинается с названия свойства, через которое идет связь со второй моделью Courses. И далее через два знака подчеркивания указывается имя свойства второй модели - например, courses__name или courses__id.
Иными словами, мы можем получить информацию о курсах студента через свойство courses, которое определено в модели Student. Однако имеется возможность получать информацию и о студентах, которые изучают определенные курсы. В этом случае надо использовать синтаксис _set.
Рассмотрим следующий программный код:
# создадим курс программирования на Python
kurs_python = Course.objects.create(name="Python")
# создаем студента и добавляем его на курс
kurs_python.student_set.create(name="Bиктop")
# отдельно создаем студента и добавляем его на курс
alex = Student(name="Aлeкcaндp")
alex.save()
kurs_python.student_set.add(alex)
# получим всех студентов курса
students = kurs_python.student_set.all()
# students будет содержать <QuerySet [<Student: Student object (2)>, <Student: Student object (3)>]>
# получим количество студентов по курсу
number = kurs_python.student_set.count()
# number будет содержать 2
# удаляем с курса одного студента
kurs_python.student_set.remove(alex)
# удаляем всех студентов с курса
kurs_python.student_set.clear()
# получим количество студентов по курсу
number = kurs_python.student_set.count()
# number будет содержать 0
Стоит учитывать, что не всегда такая организация связи «многие-ко-многим» может подойти. Например, в нашем случае создается промежуточная таблица, которая хранит только id студента и id курса.
Если нам надо в промежуточной таблице хранить еще какие-либо данные - например, дату зачисления студента на курс, его оценки и т. д., то такая конфигурация не подойдет. И тогда правильнее будет создать промежуточную сущность вручную (например, запрос или хранимую процедуру), которая связана отношением «один-ко-многим» с обеими моделями.