Рекурсивные отношения моделей в Django

| Python

В разработке современных веб-приложений часто возникает ситуации, когда техническое задание подразумевает использование рекурсивных отношений. Одним из хорошо известных примеров такого рода является определение объектов сотрудников и их отношение к менеджерам, которые также являются сотрудниками. Обратите внимание на циклический характер этого утверждения. Это именно то, что подразумевается под рекурсивными отношениями. В этой статье будет представлено простое 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-диаграмма определённого рекурсивного отношения.

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». Если всё было правильно сделано, то увидите таблицу с перечнем сотрудников.