Django 5 для начинающих

Прогресс по курсу:  9/1004

3.4 Организация связей между таблицами
3 из 11 шагов пройдено
0 из 53 баллов  получено

Организация связей между таблицами "многие-ко-многим"

Связь «многие-ко-многим» - это связь, при которой множественным записям из одной таблицы (А) могут соответствовать множественные записи из другой таблицы (В).

Примером такой связи может служить учебное заведение, где преподаватели обучают учащихся. В большинстве учебных заведений (школа, университет) каждый преподаватель обучает многих учащихся, а каждый учащийся может обучаться у нескольких преподавателей.

Еще один пример- это книги и авторы книг. У одной книги может быть несколько авторов, в то же время у одного автора может быть несколько книг.

Связь «многие-ко-многим» создается с помощью трех таблиц: две из них (А и В) - "источники" и одна таблица - соединительная. Первичный ключ соединительной таблицы (А-В) - составной. Она состоит из двух полей: двух внешних ключей, которые ссылаются на первичные ключи таблиц А и В. Все первичные ключи должны быть уникальными. Это подразумевает и то, что комбинация полей А и В должна быть уникальной в таблице А-В.

Еще одним примером такой связи являются номера гостиницы и её гости. Между таблицами «Гости» и «Комнаты» (или номера) существует связь «многие-ко-многим»: одна комната может быть заказана многими гостями в течение определенного времени, и в течение этого же промежутка времени гость может заказать в гостинице разные комнаты.

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

Вследствие природы отношения «многие-ко-многим» совершенно неважно, какая из таблиц является родительской, а какая - дочерней, потому что по своей сути такой тип отношений является симметричным.

Для представления отношения «многие-ко-многим» 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_coursemanytomany_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 курса.

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


  • Комментариев
Будьте вежливы и соблюдайте наши принципы сообщества. Пожалуйста, не оставляйте решения и подсказки в комментариях, для этого есть отдельный форум.
Оставить комментарий

помощью трех таблиц: две их(з) них (А и В) - "источники"

тут опечатка)

@Нарбеков_Марсель, Спасибо, поправил.

Здравствуйте! Подскажите пожалуйста, не могу никак понять, при создании объекта, когда используем student = Student.objects.create(name='Иван'), а когда надо использовать просто student = Student(name='Иван')?

@Мария_Толстова, Можно использовать и то, и то. Изучали в этой лекции, если вкратце, то:

Для добавления данных методом create() для нашей модели можно использовать следующий код:

second_post = Post.objects.create(text='Мой второй текст в БД')

Однако, в своей сути, метод create использует другой метод - save, и его мы также можем использовать самостоятельно для добавления объекта в БД:

>>> third_post = Post(text='Моя третья запись в БД')
>>> third_post.save()

@Илья_Перминов, спасибо большое! Поняла))

Course с маленькой буквы наверное надо ?

Изменен Дмитрий Чекмасов

@Дмитрий_Чекмасов, Не совсем, тут правильнее Student написать с большой буквы, так как это имя модели(класса). Поправили в лекции.

@Илья_Перминов, были и такие мысли )) но по аналогии с таблицей manytomany_student подумал что так же с маленькой будет )

Рассмотрев типы связей в моделях, остался один вопрос в части типа многие ко многим. Здесь таблицы симметричные и нет родительской или дочерней.  Я правильно понял - если мы используем модель в которой есть связующее свойство (в нашем случае Student) то через это свойство мы получаем необходимые связанные данные, а если модель не имеет в себе связующего свойства (модель Course), то для получения данных из связанных моделей необходимо использовать синтаксис _set ? То есть с точки зрения получения данных из симметричных моделей методы получения немного не симметричны? Заранее спасибо.  

@Евгений_Епишкин, Да, вы все верно поняли. Мы или используем:

>>> kurs = Course.objects.create(name="Django4")
>>> ilya = Student(name="Илья")
>>> ilya.save()
>>> ilya.courses.add(kurs)

Или в обратную сторону через _set как:

>>> kurs = Course.objects.create(name="Django4")
>>> max = Student(name="Максим")
>>> max.save()
>>> kurs.student_set.add(max)

Тоесть в одном случае мы обращаемся через поле связи, в другом через _set

Изменен Илья Перминов
Спасибо большое за разъяснения, теперь всё встало на свои места!

1. В этом примере модель course представляет собой учебные курсы, а модель student - студентов.
Здесь нет родительской и дочерней таблицы, они равнозначны

2.В результате миграции на основе моделей course и Student в базе данных SQLite автоматически будут созданы уже не две, а три таблицы: manytomany_coursemanytomany_student и manytomany_student_courses.

Думаю лучше использовать в описании моделей названия с заглавного символа "Course" и "Student" соответственно. В python регистр важен.

@Евгений_Епишкин, Ага, спасибо, поправил.

Не могу понять в чем преимущество использования _set, когда можно сделать тоже самое через __?

Для примера:
Student.object.filter(courses__name='Python')
kurs_python.student_set.all()

@Михаил, Давайте рассмотрим эту модель:

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


class Product(models.Model):
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    name = models.CharField(max_length=30)
    price = models.IntegerField()

У нее есть связь между продуктом и компанией, но обратной связи уже нет и мы получим ошибку.

>>> product = Product.objects.get(id=1)
>>> product.company
<Company: Company object (1)>

company = Company.objects.get(id=1)
>>> company.product
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'Company' object has no attribute 'product'

Чтобы получить данные в "обратном направлении", нам приходится использовать специальное свойство _set

>>> company.product_set.all()
<QuerySet [<Product: Product object (1)>]>

Для отношения многие ко многим, там ситуация такая же, как и у отношений один ко многим.

@Илья_Перминов,  Понял) спасибо за столь наглядное и понятное объяснение))

all_courses = Student.objects.get(name="Bиктop").courses.all()
В этой команде есть косячный символ в имени студента. Выдает ошибку:
manytomany.models.Student.DoesNotExist: Student matching query does not exist.

Если переписать руками, то норм.

Изменен Василий Шопин

@Василий_Шопин, исправил, спасибо.

@Василий_Шопин, по моему опыту, я бы посоветовала никогда не копипастить во время обучения. Тогда в реальных проектах руки сами пишут рутинный  код, а голова работает на поиск интересных решений ) Копипаст вообще зло,  не дает подняться выше кодера.

@Ольга_Миронова, Соглашусь, я т.к работаю, то часто нет много времени((
приходится ускоряться как только можно

@Василий_Шопин, Прошу прощения, это я расслабилась. Неделю назад сломала правую руку, и у меня теперь куча свободного времени - по работе грузят только совещанками, домашних дел нет... Я уже пожалела, что не выдержала и написала. Очень хороший курс, а народ к словам и буковкам придирается.

@Ольга_Миронова, Терпения и выздоровления вам)

@Ольга_Миронова, скорейшего выздоровления.

@Дмитрий_Селезнев, Спасибо)

@Ольга_Миронова, Выздоравливайте)
Так "словам и буковкам придирается" это же только на пользу курсу, последующие ученики не столкнутся с ошибками в тексте и общее впечатление будет лучше)

@Нарбеков_Марсель, Согласна)  Через пару-тройку дней я тоже пришла к такому выводу. Я сознательно открываю курс и pycharm на разных компьютерах, поэтому не вижу мелких косяков. Но курс непрерывно развивается, спасибо преподавателям, и хорошо, что учащиеся выступают в роли тестировщиков. Но копипастить все равно не советую )

Подскажите пожалуйста, я правильно понял, что для данного типа связи "многие-ко-многим" методы clear() и remove() работают без добавления в модель null=True, так как модели равнозначны и нет зависимой модели, и соответственно в каждой из них могут существовать сущности независимо друг от друга. Предполагаю что параметр null=True устанавливается в связующей таблице по умолчанию?

@Максим_Михеев, Да, все верно поняли.