Рекурсивные отношения моделей в Django
В разработке современных веб-приложений часто возникает ситуации, когда техническое задание подразумевает использование рекурсивных отношений. Одним из хорошо известных примеров такого рода является определение объектов сотрудников и их отношение к менеджерам, которые также являются сотрудниками. Обратите внимание на циклический характер этого утверждения. Это именно то, что подразумевается под рекурсивными отношениями. В этой статье будет представлено простое Django приложения для работы с персоналом с рекурсивными отношениями между сотрудниками и менеджерами.
Настройка Django проекта
Чтобы начать работу с проектом, необходимо создать новую виртуальную среду Python 3, а затем выбрать её активной.
$ python3 -m venv /var/env
$ source /var/env/bin/activate
Внутри виртуальной среды установим Django.
(venv) $ pip install django
После установки фреймворка воспользуемся Django утилитами для создания шаблона проекта, который будет называться webapp.
(venv) $ django-admin startproject webapp
Перейдём в новый каталог webapp, для того чтобы воспользоваться скриптом manage.py который сгенерирует заготовку приложения hrmgmt внутри проекта.
(venv) $ cd webapp
(venv) $ python manage.py startapp hrmgmt
Завершающая часть настройки проекта включает в себя подключение к проекту webapp информации о приложении hrmgmt. Для этого в файле webapp/settings.py найдите раздел с списком INSTALLED_APPS и добавьте туда запись hrmgmt.apps.HrmgmtConfig.
# подключение приложений к проекту
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'hrmgmt.apps.HrmgmtConfig',
]
Настройка маршрутов
Каталог webapp, соответствующий названию проекта, содержит основные параметры настройки и url точки входа для встроенного администрирования и дополнительных пользовательских приложений. Поэтому в webapp/urls.py добавим следующий код, чтобы направлять все url маршруты с префиксом /hr в приложение hrmgmt.
# webapp/urls.py
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^hr/', include('hrmgmt.urls'))
]
В пользовательском приложении hrmgmt создадим новый файл urls.py и поместим туда следующий код. Он определяет view, которое вернёт список сотрудников. В приведенном ниже коде используется регулярное выражение, указывающее, что при запросе маршрута с /hr/ функция index должна обрабатывать запрос и возвратить ответ.
# hrmgmt/urls.py
from django.conf.urls import url
import views
urlpatterns = [
# /hr/
url(r'^$', views.index, name='index')
]
Реализация функции index
Теперь реализуем функцию index для обработки запросов на маршрут /hr/ с возвращением текстового ответа, чтобы сообщить, что всё было правильно настроено. Позже вернёмся к этой функции и преобразуем её в более полезную.
hrmgmt/views.py содержит следующий код:
# hrmgmt/views.py
from django.http import HttpResponse
def index(request):
response = "Cписок сотрудников"
return HttpResponse(response)
Внутри каталога webapp запустим сервер разработки Django и проверим, что правильно настроили маршрут и функцию view:
(venv) $ python manage.py runserver
Теперь запустим браузер и введём туда http://localhost:8000/hr/. Если всё было правильно сделано, то будет показана текстовая строка «Cписок сотрудников»
Проектирование класса модели
В этом разделе определим класс модели, которая будет преобразовываться в таблицы базы данных. В файл hrmgmt/models.py добавим следующий код:
# hrmgmt/models.py
from django.db import models
class Employee(models.Model):
STANDARD = 'STD'
MANAGER = 'MGR'
SR_MANAGER = 'SRMGR'
PRESIDENT = 'PRES'
EMPLOYEE_TYPES = (
(STANDARD, 'base employee'),
(MANAGER, 'manager'),
(SR_MANAGER, 'senior manager'),
(PRESIDENT, 'president')
)
role = models.CharField(max_length=25, choices=EMPLOYEE_TYPES)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
manager = models.ForeignKey('self', null=True, related_name='employee')
def __str__(self):
return "<Employee: {} {}>".format(self.first_name, self.last_name)
Прежде всего следует отметить, что объявляется класс Python с именем Employee, который наследуется от класса django.db.models.Model. Это наследование даёт классу Employee функциональность доступа к базе данных через Django ORM.
Далее приведены определения четырех полей класса, которые являются константами (STANDARD, MANAGER, SR_MANAGER, PRESIDENT) и их использование для дальнейшего определения константы поля EMPLOYEE_TYPES типа кортеж. Это похоже на перечисления, которые определяют разные роли, которые может выполнять сотрудник. Далее, кортеж констант передается определению поля класса role для обозначения допустимых значений.
Далее поля first_name и last_name определяются как CharField длиной не более 100 символов.
Заключительное поле manager, пожалуй, является наиболее значимым. Оно является внешним ключом (FK), который определяет рекурсивную взаимосвязь между сотрудниками и их менеджерами. Это означает, что неявный автоинкрементный целочисленный столбец id, который Django создаёт в наследуемых от django.db.models.Model моделях, будет доступен как значение внешнего ключа для того же класса (или таблицы).
Это соответствует нашему требованию: «у сотрудника может быть только один непосредственный менеджер или его нет, в случае с президентом, также сотрудник может быть менеджером для множества других сотрудников». Определим self как первый параметр модели. Вызов ForeignKey, Django установит это как рекурсивное отношение. Затем, указав null = True в поле модели позволит сотруднику быть без менеджера, который в нашем примере является президентом.
Ниже приведена ER-диаграмма определённого рекурсивного отношения.
Миграция базы данных
Чтобы преобразовать код, который использовали для определения класса Employee в DDL SQL, снова воспользуемся Django утилитами, доступные через скрипт manage.py.
(venv) $ python manage.py makemigrations
(venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, hrmgmt, sessions
Running migrations:
Applying hrmgmt.0001_initial... OK
По умолчанию Django хранит все служебные таблицы и таблицы с данными проекта в БД sqlite. Первая инструкция создаёт миграционные Python скрипты определяющие необходимые таблицы и их поля, вторая команда запустит процесс непосредственного создания структур данных в БД.
Можно также просмотреть фактический DDL SQL, который создаст таблицу, выполнив следующую команду:
(venv) $ python manage.py sqlmigrate hrmgmt 0001
BEGIN;
--
-- Create model Employee
--
CREATE TABLE "hrmgmt_employee" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "role" varchar(25) NOT NULL, "first_name" varchar(100) NOT NULL, "last_name" varchar(100) NOT NULL, "manager_id" integer NULL REFERENCES "hrmgmt_employee" ("id"));
CREATE INDEX "hrmgmt_employee_manager_id_43028de6" ON "hrmgmt_employee" ("manager_id");
COMMIT;
Наполняем БД данными из Django shell
Запустим интерпретатор с контекстом приложения Django:
(venv) $ python manage.py shell
Теперь, когда интерпретатор Python запущен и работает, введём следующие команды:
>>> from hrmgmt.models import Employee
>>> janeD = Employee.objects.create(first_name='Jane', last_name='Doe', role=Employee.PRESIDENT)
>>> johnD = Employee.objects.create(first_name='John', last_name='Doe', role=Employee.MANAGER, manager=janeD)
>>> joeS = Employee.objects.create(first_name='Joe', last_name='Scho', role=Employee.STANDARD, manager=johnD)
>>> johnB = Employee.objects.create(first_name='John', last_name='Brown', role=Employee.STANDARD, manager=johnD)
Вышеприведенный код создаёт четыре тестовых сотрудника. Jane Doe - президент. Затем John Doe назначен на роль менеджера, и им управляет его мать Jane Doe (да, здесь явно существует какой-то непотизм). В подчинении у John Doe находятся Joe Scho и John Brown, оба являются рядовыми сотрудниками.
Теперь протестируем отношения между объектами Employee, проверив вывод переменной johnD:
>>> johnD.employee.all()
<QuerySet [<Employee: Joe Scho>, <Employee: John Brown>]>
Далее janeD:
>>> janeD.employee.all()
<QuerySet [<Employee: John Doe>]>
Аналогичным образом, проверим поле manager.
>>> johnD.manager
<Employee: Jane Doe>
Похоже, что всё работает так, как ожидалось.
Настройка view
В каталоге hrmgmt, создадим другой каталог под названием templates. Затем в каталоге templates создадим ещё один каталог с именем hrmgmt. Наконец, в каталоге hrmgmt/templates/hrmgmt создадим html файл с именем index.html. В этом файле напишем код отображающий список сотрудников.
Скопируйте и вставьте следующий код:
<!-- hrmgmt/templates/hrmgmt/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Employee Listing</title>
<meta charset="UTF-8">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Employee Listing</h1>
</div>
</div>
<div class="row">
<dov class="col-md-12">
<table class="table table-striped">
<thead class="thead-inverse">
<tr>
<th>Employee ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Role</th>
<th>Manager</th>
</tr>
</thead>
<tbody class='table-striped'>
{% for employee in employees %}
<tr>
<td>{{ employee.id }}</td>
<td>{{ employee.first_name }}</td>
<td>{{ employee.last_name }}</td>
<td>{{ employee.get_role_display }}</td>
<td>{% if employee.manager %}{{ employee.manager.first_name }} {{ employee.manager.last_name }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</dov>
</div>
</div>
</body>
</html>
Этот файл известен как шаблон веб-фреймворка Django. Шаблоны представляют собой базу для построения динамически генерируемого HTML на основе переданных ему данных. В нашем случае данные, передаваемые нашему шаблону index, представляют собой список сотрудников.
Чтобы подключить шаблон, нам нужно внести пару изменений в функцию index. А именно, нужно импортировать вспомогательную функцию render из Django shortcuts, а вместо return HttpResponse вернём вызов render, в который будут переданы объект requests, строковый путь к шаблону и словарь контекста, содержащий данные для рендеринга шаблона.
# hrmgmt/views.py
from django.shortcuts import render
from .models import Employee
def index(request):
employees = Employee.objects.order_by('id').all()
context = {'employees': employees}
return render(request, 'hrmgmt/index.html', context)
Запустим сервер разработки Django и в браузере откроем URL http://localhost:8000/hr/, затем нажмём на «Enter». Если всё было правильно сделано, то увидите таблицу с перечнем сотрудников.