Отношение Django «один ко многим» (one-to-many) в Python на примерах
Для моделирования отношения «один ко многим» в Django используется ForeignKey в Python.
- Что такое отношение «один ко многим» в Django?
- Взаимодействие с моделями
- Использование select_related() для объединения сотрудника с отделом
Что такое отношение «один ко многим» в Django?
В отношении «one-to-many» (один ко многим) строка в таблице связана с одной или несколькими строками в другой таблице. Например, в отделе может быть один или несколько сотрудников, и каждый сотрудник принадлежит к одному отделу.
Связь между отделами и сотрудниками — это связь «один ко многим». И наоборот, связь между сотрудниками и отделами — это связь «многие к одному».
Чтобы создать отношение «один ко многим» в Django, вы используете ForeignKey. Например, ниже ForeignKey используется для создания отношения «один ко многим» между моделями Department и Employee:
from django.db import models class Contact(models.Model): phone = models.CharField(max_length=50, unique=True) address = models.CharField(max_length=50) def __str__(self): return self.phone class Department(models.Model): name = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) def __str__(self): return self.name class Employee(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) contact = models.OneToOneField( Contact, on_delete=models.CASCADE, null=True ) department = models.ForeignKey( Department, on_delete=models.CASCADE ) def __str__(self): return f'{self.first_name} {self.last_name}'
Как это работает.
- Сначала определим класс модели Department.
- Во-вторых, измените класс Employee, добавив отношение «один ко многим» с помощью ForeignKey:
department = models.ForeignKey( Department, on_delete=models.CASCADE )
В ForeignKey мы передаем Department в качестве первого аргумента и ключевой аргумент on_delete как models.CASCADE.
Параметр on_delete=models.CASCADE указывает, что при удалении отдела все сотрудники, связанные с этим отделом, также удаляются.
Обратите внимание, что вы определяете поле ForeignKey во многих сторонах отношения.
- В-третьих, выполните миграцию с помощью команды makemigrations:
python manage.py makemigrations
Django выдаст следующее сообщение:
It is impossible to add a non-nullable field 'department' to employee without specifying a default. This is because the database needs something to populate existing rows. Please select a fix: 1) Provide a one-off default now(will be set on all existing rows with a null value for this column) 2) Quit and manually define a default value in models.py. Select an option:
В модели Employee мы определяем поле department как ненулевое поле. Поэтому Django необходимо значение по умолчанию для обновления поля department(или столбца department_id) для существующих строк в таблице базы данных.
Даже если в таблице hr_employees нет строк, Django также попросит вас указать значение по умолчанию.
Как ясно показано в сообщении, Django предоставляет вам два варианта. Вам нужно ввести 1 или 2, чтобы выбрать соответствующий вариант.
В первом варианте Django запрашивает одноразовое значение по умолчанию. Если ввести 1, то Django покажет командную строку Python:
>>>
В этом случае вы можете использовать любое допустимое значение в Python, например, None:
>>> None
После того, как вы укажете значение по умолчанию, Django выполнит миграции следующим образом:
Migrations for 'hr': hr\migrations\0002_department_employee_department.py - Create model Department - Add field department to employee
В базе данных Django создает hr_department и добавляет столбец department_id в таблицу hr_employee. Столбец department_id таблицы hr_employee ссылается на столбец id таблицы hr_department.
Если вы выберете второй вариант, введя число 2, Django позволит вам вручную определить значение по умолчанию для поля department. В этом случае вы можете добавить значение по умолчанию для поля department в models.py следующим образом:
department = models.ForeignKey( Department, on_delete=models.CASCADE, default=None )
После этого вы можете выполнить миграции с помощью команды makemigrations:
python manage.py makemigrations
Будет показан следующий вывод:
Migrations for 'hr': hr\migrations\0002_department_employee_department.py - Create model Department - Add field department to employee
Если в таблице hr_employee есть какие-либо строки, вам необходимо удалить их все перед переносом новых миграций:
python manage.py shell_plus >>> Employee.objects.all().delete()
Затем вы можете внести изменения в базу данных с помощью команды миграции:
python manage.py migrate
Выход:
Operations to perform: Apply all migrations: admin, auth, contenttypes, hr, sessions Running migrations: Applying hr.0002_department_employee_department... OK
Другой способ решения этой проблемы — сброс миграций, о котором мы поговорим в руководстве по сбросу миграций.
Взаимодействие с моделями
- Сначала выполните команду shell_plus:
python manage.py shell_plus
- Во-вторых, создайте новый отдел с названием ИТ:
>>> d = Department(name='IT',description='Information Technology') >>> d.save()
- В-третьих, создайте двух сотрудников и назначьте их в IT-отдел:
>>> e = Employee(first_name='John',last_name='Doe',department=d) >>> e.save() >>> e = Employee(first_name='Jane',last_name='Doe',department=d) >>> e.save()
- В-четвертых, получите доступ к объекту отдела из объекта сотрудника:
>>> e.department <Department: IT> >>> e.department.description 'Information Technology'
- В-пятых, получите всех сотрудников отдела, используя атрибут employee_set следующим образом:
>>> d.employee_set.all() <QuerySet [<Employee: John Doe>, <Employee: Jane Doe>]>
Обратите внимание, что мы не определили свойство employee_set в модели Department. Внутренне Django автоматически добавил свойство employee_set в модель Department, когда мы определили отношение «один ко многим» с помощью ForeignKey.
Метод all() объекта employee_set возвращает QuerySet, содержащий всех сотрудников, принадлежащих отделу.
Использование select_related() для объединения сотрудника с отделом
Выйдите из shell_plus и выполните его снова. На этот раз мы добавляем опцию —print-sql для отображения сгенерированного SQL, который Django выполнит для базы данных:
python manage.py shell_plus --print-sql
Следующий код возвращает первого сотрудника:
>>> e = Employee.objects.first() SELECT "hr_employee"."id", "hr_employee"."first_name", "hr_employee"."last_name", "hr_employee"."contact_id", "hr_employee"."department_id" FROM "hr_employee" ORDER BY "hr_employee"."id" ASC LIMIT 1 Execution time: 0.003000s [Database: default]
Чтобы получить доступ к отделу первого сотрудника, используйте атрибут отдела:
>>> e.department SELECT "hr_department"."id", "hr_department"."name", "hr_department"."description" FROM "hr_department" WHERE "hr_department"."id" = 1 LIMIT 21 Execution time: 0.013211s [Database: default] <Department: IT>
В этом случае Django выполняет два запроса. Первый запрос выбирает первого сотрудника, а второй запрос выбирает отдел выбранного сотрудника.
Если вы выбираете N сотрудников для отображения их на веб-странице, то вам нужно выполнить N + 1 запрос, чтобы получить как сотрудников, так и их отделы. Первый запрос(1) выбирает N сотрудников, а N запросов выбирают N отделов для каждого сотрудника. Эта проблема известна как проблема запроса N + 1.
Чтобы исправить проблему запроса N + 1, можно использовать метод select_related() для выбора как сотрудников, так и отделов с помощью одного запроса. Например:
>>> Employee.objects.select_related('department').all() SELECT "hr_employee"."id", "hr_employee"."first_name", "hr_employee"."last_name", "hr_employee"."contact_id", "hr_employee"."department_id", "hr_department"."id", "hr_department"."name", "hr_department"."description" FROM "hr_employee" INNER JOIN "hr_department" ON("hr_employee"."department_id" = "hr_department"."id") LIMIT 21 Execution time: 0.012124s [Database: default] <QuerySet [<Employee: John Doe>, <Employee: Jane Doe>]>
В этом примере Django выполняет только один запрос, который объединяет таблицы hr_employee и hr_department.