Отношение Django «один ко многим» (one-to-many) в Python на примерах

Для моделирования отношения «один ко многим» в Django используется ForeignKey в Python.

Содержание

Что такое отношение «один ко многим» в 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

Пример отношения one-to-many

В базе данных 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, содержащий всех сотрудников, принадлежащих отделу.

Выйдите из 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.

Похожие посты
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *