Relaciones de modelos recursivos en Django

Surge muchas veces en el desarrollo de aplicaciones web modernas donde los requisitos comerciales describen inherentemente relaciones que son recursivas. Una...

La necesidad de relaciones recursivas

Surge muchas veces en el desarrollo de aplicaciones web modernas donde los requisitos comerciales inherentemente describen relaciones que son recursivo. Un ejemplo bien conocido de una regla comercial de este tipo se encuentra en la descripción de los empleados y su relación con sus gerentes, que también son empleados. Note la naturaleza circular de esa declaración. Esto es exactamente lo que significa una relación recursiva. En este artículo, desarrollaremos una demostración básica en Django de una aplicación de listado de empleados de recursos humanos (HR) con esta relación recursiva entre empleados y gerentes.

El código de este artículo se puede encontrar en este repositorio de GitHub.

Configuración de la estructura del proyecto Django

Para comenzar con un proyecto de Django, querrá crear un nuevo entorno virtual de python (preferiblemente Python3). Si no está familiarizado con los entornos virtuales, consulte Este artículo. Una vez dentro de su entorno virtual activado, pip instale Django.

1
(venv) $ pip install django

Con Django instalado, puede utilizar las utilidades de administración de Django para generar el modelo estándar del proyecto, que llamaremos "webapp". Puede obtener más información sobre la configuración del proyecto Django en nuestro artículo, Frasco contra Django.

1
(venv) $ django-admin startproject webapp

Ahora cd en el nuevo directorio de la aplicación web para que podamos utilizar otro conjunto de herramientas de Django a través del script manage.py. Usamos esto para crear la aplicación de nuestro proyecto, que llamaremos "hrmgmt". Esto crea otro directorio llamado "hrmgmt", que es donde residirá el código de esta aplicación.

1
2
(venv) $ cd webapp
(venv) $ python manage.py startapp hrmgmt

La última parte de la configuración del proyecto incluye informar al proyecto (aplicación web) sobre la aplicación "hrmgmt". En "webapp/settings.py" busque la sección con un comentario de "Definición de la aplicación" arriba de la lista INSTALLED_APPS y agregue una entrada de hrmgmt.apps.HrmgmtConfig, así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'hrmgmt.apps.HrmgmtConfig'
]

Configurando las Rutas

En Django, el directorio que coincide con el nombre del proyecto, "webapp" en nuestro caso, es donde residen las configuraciones principales y el punto de entrada a las rutas para la aplicación de administración integrada y cualquier aplicación personalizada adicional. Entonces, en "webapp/urls.py" use el siguiente código para dirigir todas las rutas con el prefijo "/hr" a la aplicación "hrmgmt".

1
2
3
4
5
6
7
8
# 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'))
]

En la aplicación "hrmgmt" personalizada, cree un nuevo archivo llamado "urls.py" y coloque el siguiente código. Esto especifica una vista que devolverá una lista de todos los empleados. El siguiente código usa una expresión regular para indicar que cuando se solicita una ruta de "/hr/" desde nuestro servidor, entonces una función de vista llamada index debe manejar la solicitud y devolver una respuesta.

1
2
3
4
5
6
7
8
9
# hrmgmt/urls.py
from django.conf.urls import url

import views

urlpatterns = [
    # /hr/
    url(r'^$', views.index, name='index')
]

A continuación, hablaremos sobre lo que hace la función de vista de índice.

Rellenar la función de vista de índice

Ahora implementemos la función de vista index antes mencionada para manejar las solicitudes a la ruta "/hr/" y devolver una respuesta de texto para informarnos que hemos configurado las cosas correctamente. Más tarde volveremos y convertiremos esto en una función de vista más adecuada para enumerar a nuestros empleados.

En hrmgmt/views.py incluye el siguiente código:

1
2
3
4
5
6
# hrmgmt/views.py
from django.http import HttpResponse

def index(request):
    response = "My List of Employees Goes Here"
    return HttpResponse(response)

Dentro del directorio de la aplicación web, inicie el servidor de desarrollo de Django y pruebe que hemos configurado nuestra función de ruta y vista correctamente:

1
(venv) $ python manage.py runserver

Ahora vaya a su navegador e ingrese http://localhost:8000/hr/ y debería ver una respuesta de texto de "Mi lista de empleados va aquí"

Diseñando nuestras Clases Modelo

¡Finalmente estamos llegando a la parte buena! En esta sección definimos nuestras clases modelo que se traducirán en tablas de base de datos, todo hecho escribiendo código Python. O usando lo que la gente de .NET ha acuñado como un enfoque de "código primero" para el diseño de bases de datos.

En hrmgmt/models.py coloque el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 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)

    def __repr__(self):
        return self.__str__()

Hay mucho en juego en estas pocas líneas de código, así que vamos a desglosarlos. Lo primero a tener en cuenta es que se declara una clase de Python llamada Employee, que hereda de la clase django.db.models.Model. Esta herencia le da a la clase Employee la funcionalidad para acceder a la base de datos a través del ORM de Django.

A continuación se encuentran las definiciones de cuatro campos de clase que son constantes (ESTÁNDAR, ADMINISTRADOR, SR_MANAGER, PRESIDENTE) y su uso para definir aún más una constante de campo de clase de tupla. Son algo así como enumeraciones que especifican los diferentes roles que puede asumir un empleado. De hecho, la constante de tupla de tuplas se pasa a la definición del campo de clase de roles para indicar qué valores se le debe permitir aceptar a la clase.

A continuación, los campos de clase first_name y last_name se definen como campos de caracteres con una longitud máxima de 100 caracteres.

El campo final que se define es quizás el más significativo, el campo gerente. Es una clave externa que define una relación recursiva entre los empleados y sus gerentes. Esto significa que la columna de identificación de enteros de incremento automático implícito que Django crea en los modelos heredados de django.db.models.Model estará disponible como un valor de clave externa para la misma clase (o tabla).

Esto satisfará nuestro caso de uso que podría establecerse como “un empleado puede tener solo un gerente directo o ningún gerente en el caso del presidente, pero un empleado puede administrar muchos empleados diferentes”. Al especificar self como el primer parámetro de la llamada model.ForeignKey, Django configurará esto como una relación recursiva. Luego, al especificar null=True, el modelo permitirá un empleado sin gerente, que en nuestro ejemplo es el que representa al presidente.

A continuación se muestra un diagrama ERD de la relación recursiva que hemos definido.

{.img-responsive}

Migración de nuestra definición de clase a la base de datos {#migración de nuestra definición de clase a la base de datos}

Para transformar el código que usamos para definir nuestra clase Employee en DDL SQL, volveremos a utilizar una utilidad de Django a la que se accede a través del script "manage.py" y que se conoce colectivamente como migraciones.

En la línea de comandos, dentro de nuestro entorno virtual, por supuesto, ejecute lo siguiente para crear las tablas predeterminadas que utilizan todas las aplicaciones de Django. De forma predeterminada, esta base de datos es una base de datos sqlite dentro de la carpeta raíz del proyecto.

1
(venv) $ python manage.py migrate

Una vez completada, podemos realizar una nueva migración que defina la tabla que respaldará nuestra clase Employee. Haga esto emitiendo los siguientes comandos y asegúrese de observar el resultado como se muestra a continuación:

1
2
3
4
5
6
(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

Puede ver el DDL SQL real que crea la tabla ejecutando el siguiente comando:

1
2
3
4
5
6
7
8
9
(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;

Exploración de modelos con Django Shell {#exploración de modelos con DjangoShell}

En la línea de comando, ingrese el siguiente comando para poner en marcha el intérprete con el contexto de nuestra aplicación Django precargado en el REPL:

1
(venv) $ python manage.py shell

Ahora que el intérprete de Python está funcionando, ingrese los siguientes comandos:

1
2
3
4
5
>>> 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)

El código anterior crea cuatro empleados ficticios. Jane Doe es la presidenta. Luego, John Doe tiene un rol de gerente y es administrado por su madre Jane Doe (sí, claramente hay algo de nepotismo aquí). Bajo la supervisión de John Doe están Joe Schmo y John Brown, quienes tienen los roles de un empleado estándar o básico.

Podemos probar nuestro campo de relación de empleado al inspeccionar el resultado de llamar a empleado en nuestra variable johnD:

1
2
>>> johnD.employee.all()
<QuerySet [<Employee: Joe Scho>, <Employee: John Brown>]>

Así como con la variable janeD:

1
2
>>> janeD.employee.all()
<QuerySet [<Employee: John Doe>]>

Del mismo modo, querremos probar nuestro campo de administrador para asegurarnos de que funciona como se desea:

1
2
>>> johnD.manager
<Employee: Jane Doe>

¡Excelente! Parece que las cosas están funcionando como se esperaba.

Configuración de nuestra vista

En el mismo directorio que nuestro directorio "hrmgmt", cree otro directorio llamado "templates". Luego dentro del directorio "templates" cree otro directorio llamado "hrmgmt". Finalmente dentro del directorio "hrmgmt/templates/hrmgmt" crea un archivo HTML llamado "index.html". Es dentro de este archivo que escribiremos el código para construir nuestra lista de empleados.

Copia y pega el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!-- hrmgmt/templates/hrmgmt/index.html -->
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Employee Listing</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
    </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>
        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
    </body>
</html>

Este archivo se conoce como modelo en el marco web de Django. Las plantillas representan un modelo para HTML reproducible que se genera dinámicamente en función de los datos que se le pasan. En nuestro caso, los datos que se pasan a nuestra plantilla "index" representan nuestra lista de empleados.

Para servir nuestra plantilla, necesitaremos hacer un par de cambios en nuestra función de vista. Es decir, necesitamos importar la función de ayuda render desde los accesos directos de Django, luego, en lugar de devolver HttpResponse, devolveremos una llamada a render, pasando el objeto request, la ruta a nuestra plantilla y un diccionario que contiene los datos a pasar a nuestra plantilla.

1
2
3
4
5
6
7
8
9
# 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)

Nuevamente, inicie nuestro servidor de desarrollo Django y en un navegador escriba http://localhost:8000/hr/ en el campo URL y luego presione "Enter". Debería ver un resultado similar a la siguiente captura de pantalla:

{.img-responsive}

Puede ver en la columna "Administrador" resultante de la tabla que hemos vinculado con éxito un Empleado a un Empleado utilizando modelos de Django.

Conclusión

En este artículo, hemos repasado el caso de uso de por qué implementaríamos una relación recursiva dentro de un modelo de Django. Repasamos el código para definir una relación tan recursiva y cómo interactuar con los modelos para conservarlos en la base de datos y luego cómo recuperarlos. Finalmente, terminamos viendo cómo mostrar la información en nuestros modelos respaldados por bases de datos en una plantilla de Django.

Si has llegado hasta aquí, me gustaría agradecerte por leer mi artículo. Espero que este artículo lo inspire a seguir investigando el desarrollo web con el marco web de Django. Como siempre, invito a todos y cada uno de los comentarios, sugerencias o críticas. cias o críticas.