Отношение Django Один-к-одному (one-to-one) в Python
В этом уроке вы узнаете об отношении «один к одному» (one-to-one) в Django и о том, как оно работает «под капотом» в Python.
- Что такое отношение Django «один к одному»?
- Миграция моделей в базу данных
- Связывание контакта с сотрудником
- Получение данных из отношения one-to-one
- Выбор связанных объектов
Что такое отношение 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_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>]>