Отношение Django Один-к-одному (one-to-one) в Python

В этом уроке вы узнаете об отношении «один к одному» (one-to-one) в Django и о том, как оно работает «под капотом» в Python.

Содержание

Что такое отношение Django «один к одному»?

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

Например, у каждого сотрудника есть контакт, и каждый контакт принадлежит одному сотруднику. Таким образом, связь между сотрудниками и контактами является связью один к одному.

Для создания отношения «один к одному» используется класс OneToOneField:

OneToOneField(to, on_delete, parent_link=False, **options)

В этом синтаксисе:

  • Параметр to определяет название модели.
  • on_delete определяет действие над контактом при удалении сотрудника.

В следующем примере класс OneToOneField используется для определения отношения «один к одному» между моделями Contact и Employee в models.py:

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 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)

    def __str__(self):
        return f'{self.first_name} {self.last_name}'

Класс Employee имеет атрибут contact, который ссылается на экземпляр класса OneToOneField.

В OneToOneField мы указываем модель Contact и параметр on_delete, который определяет поведение при удалении объекта сотрудника.

Параметр on_delete=models.CASCADE означает, что при удалении объекта Employee объект Contact, связанный с этим Employee, также будет автоматически удален.

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

Миграция моделей в базу данных

Сначала выполните миграции с помощью команды makemigrations:

python manage.py makemigrations
Migrations for 'hr':
  hr\migrations\0002_contact_employee_contact.py
    - Create model Contact
    - Add field contact to employee

Во-вторых, примените миграции к базе данных с помощью команды migrate:

python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, hr, sessions
Running migrations:
  Applying hr.0002_contact_employee_contact... OK

За кулисами Django создает в базе данных две таблицы hr_contact и hr_employee:

Создание таблиц hr_contact и hr_employee

Таблица hr_employee содержит столбец contact_id, который является внешним ключом, связанным с идентификатором(первичным ключом) таблицы hr_contact.

Связывание контакта с сотрудником

Для взаимодействия с моделями Employee и Contact выполните команду shell_plus с опцией —print-sql:

python manage.py shell_plus --print-sql

Параметр —print-sql выводит команду SQL, которую выполняет Django.

Сначала создайте новый объект Employee и сохраните его в базе данных:

>>> e = Employee(first_name='John',last_name='Doe')
>>> e.save()

Django выполняет следующую команду SQL:

INSERT INTO "hr_employee"("first_name", "last_name", "contact_id")
VALUES('John', 'Doe', NULL) 
RETURNING "hr_employee"."id"

Во-вторых, создайте и сохраните новый контакт в базе данных:

>>> c = Contact(phone='40812345678', address='101 N 1st Street, San Jose, CA')
>>> c.save()

Django также выполняет следующую команду INSERT:

INSERT INTO "hr_contact"("phone", "address")
VALUES('40812345678', '101 N 1st Street, San Jose, CA') 
RETURNING "hr_contact"."id"

В-третьих, свяжите контакт с сотрудником:

>>> e.contact = c
>>> e.save()

Django обновляет значение столбца contact_id в таблице hr_employee до значения столбца id в таблице hr_contact.

UPDATE "hr_employee"
   SET "first_name" = 'John',
       "last_name" = 'Doe',
       "contact_id" = 1
 WHERE "hr_employee"."id" = 3

Получение данных из отношения one-to-one

Сначала найдем сотрудника по имени John Doe:

>>> e = Employee.objects.filter(first_name='John',last_name='Doe').first()
<Employee: John Doe>

Django выполняет оператор SELECT, который получает строку с first_name «John» и last_name «Doe».

У hr_employee может быть несколько сотрудников с одинаковыми именем и фамилией. Поэтому filter() возвращает QuerySet.

Чтобы получить первую строку в QuerySet, мы используем метод first(). Метод first() возвращает один экземпляр класса Employee.

SELECT "hr_employee"."id",
       "hr_employee"."first_name",
       "hr_employee"."last_name",
       "hr_employee"."contact_id"
  FROM "hr_employee"
 WHERE("hr_employee"."first_name" = 'John' AND "hr_employee"."last_name" = 'Doe')
 ORDER BY "hr_employee"."id" ASC
 LIMIT 1

Обратите внимание, что запрос не получает контактные данные из таблицы hr_contact. Он получает данные только из таблицы hr_employee.

При доступе к контактному атрибуту сотрудника:

>>> e.contact

… Django выполняет второй оператор SELECT для получения данных из таблицы hr_contact:

SELECT "hr_contact"."id",
       "hr_contact"."phone",
       "hr_contact"."address"
  FROM "hr_contact"
 WHERE "hr_contact"."id" = 1
 LIMIT 21
<Contact: 40812345678>

Следующий код получает контакт с идентификатором 1:

>>> c = Contact.objects.get(id=1)

При связывании контакта с сотрудником вы можете получить доступ к сотруднику из объекта контакта:

>>> c.employee
<Employee: John Doe>

Django выполняет оператор SELECT для получения данных из таблицы hr_employee:

SELECT "hr_employee"."id",
       "hr_employee"."first_name",
       "hr_employee"."last_name",
       "hr_employee"."contact_id"
  FROM "hr_employee"
 WHERE "hr_employee"."contact_id" = 1
 LIMIT 21

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

Давайте создадим еще один контакт, который не связан ни с одним сотрудником:

>>> c = Contact(phone='4081111111',address='202 N 1st Street, San Jose, CA')
>>> c.save()

Django выполнит следующий оператор INSERT:

INSERT INTO "hr_contact"("phone", "address")
VALUES('4081111111', '202 N 1st Street, San Jose, CA') 
RETURNING "hr_contact"."id"

Если вы найдете контакт, который не связан ни с одним сотрудником, и попытаетесь получить доступ к этому сотруднику, вы получите исключение RelatedObjectDoesNotExist.

Выбор связанных объектов

Сначала создайте нового сотрудника:

>>> e = Employee(first_name='Jane',last_name='Doe')
>>> e.save()
INSERT INTO "hr_employee"("first_name", "last_name", "contact_id")
VALUES('Jane', 'Doe', NULL) RETURNING "hr_employee"."id"
Execution time: 0.003079s [Database: default]

Во-вторых, получите всех сотрудников:

>>> Employee.objects.all()

Django возвращает двух сотрудников:

<QuerySet [<Employee: John Doe>, <Employee: Jane Doe>]>

Если вам необходимо отобразить всех сотрудников и их контакты на одной странице, то у вас возникает проблема запроса N+1:

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

Чтобы избежать этого, вы можете запросить всех сотрудников и контакты с помощью одного запроса, используя метод select_related():

>>> Employee.objects.select_related('contact').all()

В этом случае Django выполняет следующий оператор LEFT JOIN:

SELECT "hr_employee"."id",
       "hr_employee"."first_name",
       "hr_employee"."last_name",
       "hr_employee"."contact_id",
       "hr_contact"."id",
       "hr_contact"."phone",
       "hr_contact"."address"
  FROM "hr_employee"
  LEFT OUTER JOIN "hr_contact"
    ON("hr_employee"."contact_id" = "hr_contact"."id")
 LIMIT 21

Django использует LEFT JOIN, который возвращает всех сотрудников из таблицы hr_employee и контакты, связанные с выбранными сотрудниками:

<QuerySet [<Employee: John Doe>, <Employee: Jane Doe>]>
Похожие посты
Добавить комментарий

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