Café y Código

5.3 Flota y Pilotos: Migraciones Evolutivas y Faker 🚛

1. El Reto: Gestión de Flota y Poblado de Datos Realistas

En el desarrollo backend, tarde o temprano te enfrentarás a dos desafíos cruciales:

  1. Diseñar modelos que protejan los activos de la empresa: Si modelas un sistema de transporte o logística y utilizas models.CASCADE de forma descuidada, dar de baja a un conductor podría borrar camiones de decenas de miles de dólares de tu base de datos.
  2. Probar el sistema con volumen y datos creíbles: Ingresar 50 vehículos y 50 pilotos a mano desde el panel administrativo de Django consume horas de trabajo manual y produce registros artificiales como "test1" o "prueba", los cuales no permiten evaluar búsquedas por RUT, nombres reales, patentes o distribución de estados operativos.

En este laboratorio práctico construiremos la aplicación flota a lo largo de 3 fases de migración auditadas con SQL real y programaremos un Controlador Web (View) con la librería Faker para sembrar datos en milisegundos con transacciones atómicas.

2. Diagrama Entidad-Relación (ER): Arquitectura del Modelo

Antes de escribir una sola línea de código en Python o ejecutar makemigrations, es fundamental visualizar cómo interactúan nuestras entidades en la base de datos relacional.

Explora el siguiente diagrama interactivo: haz clic en cualquier columna para inspeccionar su equivalente en Django ORM y sus restricciones SQL:

📐

Diagrama de Relación Entidad-Relación (ER)Flota · 1 a N Opcional

Estructura relacional entre Conductores y Vehículos con preservación de activos vía SET_NULL.

👤

flota_piloto

Modelo: Piloto (Conductor)
1 (Uno)
⟷
1 a N
Un piloto puede conducir 0 o varios vehículos.
ON DELETE SET NULL
🚛

flota_vehiculo

Modelo: Vehiculo (Activo)
N (Muchos / Opcional)
🔍 Inspector de Campo SQL y ORM:piloto_asignado_id (bigint NULL)
FK

Clave Foránea hacia Piloto. Si el piloto es despedido o eliminado, la BD pone este campo en NULL gracias a ON DELETE SET NULL, preservando el vehículo de la empresa.

# Definición en Django ORM (models.py):piloto_asignado = models.ForeignKey(Piloto, on_delete=models.SET_NULL, null=True, blank=True, related_name='vehiculos')

3. Laboratorio Interactivo: Migraciones y Controlador Faker

Interactúa con el simulador a continuación para explorar cada paso de la migración en código Python y SQL, ejecutar el controlador de Faker en vivo y consultar la flota resultante:

🚛

Laboratorio Interactivo: Flota, Migraciones y Controlador FakerEn Vivo

Explora las 3 fases de migraciones en SQL y genera datos realistas con el controlador de Faker.

📄 flota/models.py (Fase 1)Python 3.12
from django.db import models

class Piloto(models.Model):
    nombre = models.CharField(max_length=100)
    rut = models.CharField(max_length=15, unique=True)
    licencia = models.CharField(max_length=50) # ej: Clase A1, A2, B
    telefono = models.CharField(max_length=20, blank=True)
    experiencia_anios = models.PositiveIntegerField(default=1)
    activo = models.BooleanField(default=True)
    fecha_ingreso = models.DateField(auto_now_add=True)

    class Meta:
        verbose_name = "Piloto"
        verbose_name_plural = "Pilotos"

    def __str__(self):
        return f"{self.nombre} ({self.licencia})"
🔍 python manage.py sqlmigrate flota 0001SQL PostgreSQL
-- Generado automáticamente por Django
CREATE TABLE "flota_piloto" (
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "nombre" varchar(100) NOT NULL,
    "rut" varchar(15) NOT NULL UNIQUE,
    "licencia" varchar(50) NOT NULL,
    "telefono" varchar(20) NOT NULL,
    "experiencia_anios" integer NOT NULL CHECK ("experiencia_anios" >= 0),
    "activo" boolean NOT NULL,
    "fecha_ingreso" date NOT NULL
);
# Comandos ejecutados en la terminal:

(env) $ python manage.py makemigrations flota --name initial_piloto

Migrations for 'flota':
  flota/migrations/0001_initial_piloto.py
    - Create model Piloto

(env) $ python manage.py migrate flota

Applying flota.0001_initial_piloto... OK

4. Fase 1: Creación del Modelo Piloto (`0001_initial.py`)

Iniciamos nuestra aplicación flota modelando a los conductores. Cada piloto posee una identificación fiscal única (RUT/DNI), licencia de conducir, teléfono y años de experiencia:

PYTHON
1# flota/models.py
2from django.db import models
3
4class Piloto(models.Model):
5 nombre = models.CharField(max_length=100, verbose_name="Nombre Completo")
6 rut = models.CharField(max_length=15, unique=True, verbose_name="RUT o DNI")
7 licencia = models.CharField(max_length=50, verbose_name="Tipo de Licencia") # Ej: A1, A2, B
8 telefono = models.CharField(max_length=20, blank=True, verbose_name="Teléfono de Contacto")
9 experiencia_anios = models.PositiveIntegerField(default=1, verbose_name="Años de Experiencia")
10 activo = models.BooleanField(default=True, verbose_name="¿En Servicio Activo?")
11 fecha_ingreso = models.DateField(auto_now_add=True, verbose_name="Fecha de Contratación")
12
13 class Meta:
14 verbose_name = "Piloto"
15 verbose_name_plural = "Pilotos"
16 ordering = ['nombre']
17
18 def __str__(self):
19 return f"{self.nombre} ({self.licencia})"

Terminal y Auditoría con `sqlmigrate`

Generamos la primera migración con makemigrations y auditamos qué sentencia SQL recibirá PostgreSQL antes de ejecutarla:

BASH
1# 1. Crear el archivo de migración
2(env) $ python manage.py makemigrations flota --name initial_piloto
3Migrations for 'flota':
4 flota/migrations/0001_initial_piloto.py
5 - Create model Piloto
6
7# 2. Auditar la sentencia SQL real que ejecutará el motor
8(env) $ python manage.py sqlmigrate flota 0001
SQL
1-- Salida de sqlmigrate flota 0001:
2CREATE TABLE "flota_piloto" (
3 "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
4 "nombre" varchar(100) NOT NULL,
5 "rut" varchar(15) NOT NULL UNIQUE,
6 "licencia" varchar(50) NOT NULL,
7 "telefono" varchar(20) NOT NULL,
8 "experiencia_anios" integer NOT NULL CHECK ("experiencia_anios" >= 0),
9 "activo" boolean NOT NULL,
10 "fecha_ingreso" date NOT NULL
11);

Nota cómo Django traduce automáticamente unique=True a una restricción UNIQUE y PositiveIntegerField a un CHECK ("experiencia_anios" >= 0) a nivel del motor relacional.

5. Fase 2: Modelo Vehículo y Foreign Key (`0002_vehiculo_relacion.py`)

Ahora definimos la entidad Vehiculo. Un vehículo de la empresa puede estar asignado a un conductor o encontrarse estacionado en patio listo para ser despachado.

⚠️ Decisión Arquitectónica Crucial: ¿CASCADE o SET_NULL?

En relaciones donde la entidad secundaria representa un activo patrimonial de la empresa (camiones, vehículos, maquinaria), jamás debes usar CASCADE. Si un piloto renuncia o es eliminado del sistema, el camión no debe borrarse. Usamos on_delete=models.SET_NULL junto a null=True, blank=True para que el vehículo simplemente quede libre (NULL).

PYTHON
1# flota/models.py (Añadir al final)
2class Vehiculo(models.Model):
3 patente = models.CharField(max_length=10, unique=True, verbose_name="Patente o Placa")
4 marca = models.CharField(max_length=50, verbose_name="Marca")
5 modelo = models.CharField(max_length=50, verbose_name="Modelo")
6 anio = models.PositiveIntegerField(verbose_name="Año de Fabricación")
7 kilometraje = models.PositiveIntegerField(default=0, verbose_name="Kilometraje Actual")
8
9 # RELACIÓN 1:N CON PILOTO (Un piloto puede tener un vehículo asignado)
10 piloto_asignado = models.ForeignKey(
11 Piloto,
12 on_delete=models.SET_NULL, # 🛡️ Preserva el camión si despiden al piloto
13 null=True, # Requerido por SET_NULL a nivel de BD
14 blank=True, # Permite dejar vacío el campo en el Admin/Formulario
15 related_name='vehiculos',
16 verbose_name="Piloto Conductor"
17 )
18
19 class Meta:
20 verbose_name = "Vehículo"
21 verbose_name_plural = "Vehículos"
22
23 def __str__(self):
24 return f"{self.patente} - {self.marca} {self.modelo}"

Terminal y Foreign Key en SQL

BASH
1(env) $ python manage.py makemigrations flota --name vehiculo_relacion
2Migrations for 'flota':
3 flota/migrations/0002_vehiculo_relacion.py
4 - Create model Vehiculo
5
6(env) $ python manage.py sqlmigrate flota 0002
SQL
1-- SQL de la Foreign Key:
2CREATE TABLE "flota_vehiculo" (
3 "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
4 "patente" varchar(10) NOT NULL UNIQUE,
5 "marca" varchar(50) NOT NULL,
6 "modelo" varchar(50) NOT NULL,
7 "anio" integer NOT NULL,
8 "kilometraje" integer NOT NULL,
9 "piloto_asignado_id" bigint NULL
10);
11
12ALTER TABLE "flota_vehiculo"
13 ADD CONSTRAINT "flota_vehiculo_piloto_asignado_id_fk"
14 FOREIGN KEY ("piloto_asignado_id")
15 REFERENCES "flota_piloto" ("id")
16 ON DELETE SET NULL; -- 🛡️ Cascada segura a nivel SQL

6. Fase 3: Evolución con Choices y CheckConstraints (`0003_estado_y_constraints.py`)

El sistema entra en producción y surgen nuevas reglas de negocio:

  • Cada vehículo debe tener un estado operativo: DISPONIBLE, EN_RUTA o MANTENIMIENTO.
  • El motor de base de datos debe impedir que cualquier desarrollador o consulta guarde kilometrajes negativos o vehículos de antes del año 1990.
PYTHON
1# flota/models.py (Actualización del modelo Vehiculo)
2from django.db.models import Q, CheckConstraint
3
4class Vehiculo(models.Model):
5 # ... campos anteriores ...
6
7 class Estado(models.TextChoices):
8 DISPONIBLE = 'DISPONIBLE', 'Disponible en patio'
9 EN_RUTA = 'EN_RUTA', 'En ruta de transporte'
10 MANTENIMIENTO = 'MANTENIMIENTO', 'En taller de mantenimiento'
11
12 # Campo nuevo: default='DISPONIBLE' permite migrar tablas con registros antiguos sin error
13 estado = models.CharField(
14 max_length=20,
15 choices=Estado.choices,
16 default=Estado.DISPONIBLE,
17 verbose_name="Estado Operativo"
18 )
19
20 class Meta:
21 verbose_name = "Vehículo"
22 verbose_name_plural = "Vehículos"
23 constraints = [
24 CheckConstraint(
25 check=Q(kilometraje__gte=0),
26 name='flota_vehiculo_km_no_negativo'
27 ),
28 CheckConstraint(
29 check=Q(anio__gte=1990),
30 name='flota_vehiculo_anio_valido'
31 )
32 ]

Inspección del `ALTER TABLE`

SQL
1-- Salida de sqlmigrate flota 0003:
2ALTER TABLE "flota_vehiculo"
3 ADD COLUMN "estado" varchar(20) DEFAULT 'DISPONIBLE' NOT NULL;
4
5ALTER TABLE "flota_vehiculo"
6 ADD CONSTRAINT "flota_vehiculo_km_no_negativo"
7 CHECK ("kilometraje" >= 0);
8
9ALTER TABLE "flota_vehiculo"
10 ADD CONSTRAINT "flota_vehiculo_anio_valido"
11 CHECK ("anio" >= 1990);

7. El Controlador en Django: Generación Masiva con Faker

Para probar la aplicación en el navegador sin registrar 100 vehículos a mano, crearemos un Controlador Web (Vista en Django) que procesa una petición HTTP, inicializa la librería faker y ejecuta una inserción masiva protegida con transaction.atomic().

7.1 Instalación de Faker

Instala la librería en tu entorno virtual activo:

BASH
1(env) $ pip install faker

7.2 Creación del Controlador (`flota/views.py`)

Implementamos la vista que atiende peticiones GET (muestra el formulario) y POST (ejecuta la generación masiva):

PYTHON
1# flota/views.py
2import random
3from faker import Faker
4from django.shortcuts import render, redirect
5from django.contrib import messages
6from django.db import transaction
7from django.conf import settings
8from django.core.exceptions import PermissionDenied
9from django.contrib.admin.views.decorators import staff_member_required
10
11from .models import Piloto, Vehiculo
12
13
14@staff_member_required
15def generar_flota_fake_view(request):
16 """
17 Controlador que genera pilotos y vehículos falsos usando Faker.
18 Solo accesible para usuarios staff y cuando DEBUG=True.
19 """
20 if not settings.DEBUG:
21 raise PermissionDenied("Este controlador está deshabilitado en producción.")
22
23 if request.method == "POST":
24 cantidad = int(request.POST.get("cantidad", 10))
25 fake = Faker(['es_ES']) # Nombres y datos localizados en español
26
27 # 🛡️ Transacción atómica: si algo falla, no se guarda nada corrupto
28 with transaction.atomic():
29 # 1. Crear Pilotos
30 pilotos_creados = []
31 for _ in range(cantidad):
32 rut_num = fake.unique.random_number(digits=8)
33 piloto = Piloto.objects.create(
34 nombre=fake.name(),
35 rut=f"{rut_num}-{random.randint(0, 9)}",
36 licencia=random.choice(['Clase A1', 'Clase A2', 'Clase B', 'Clase A4']),
37 telefono=fake.phone_number()[:20],
38 experiencia_anios=random.randint(1, 20),
39 activo=True
40 )
41 pilotos_creados.append(piloto)
42
43 # 2. Catálogo de marcas y modelos realistas
44 catalogo = {
45 'Toyota': ['Hilux 4x4', 'RAV4', 'Corolla'],
46 'Hyundai': ['Tucson', 'Accent Prime', 'H-1 Van'],
47 'Volvo': ['FH16 750', 'FM Electric', 'VM 330'],
48 'Mercedes-Benz': ['Sprinter 516', 'Actros 2645', 'Vito Tourer'],
49 'Scania': ['R500 Super', 'P320 XT', 'G410']
50 }
51 marcas = list(catalogo.keys())
52
53 # 3. Crear Vehículos y vincular aleatoriamente
54 for i in range(cantidad):
55 marca = random.choice(marcas)
56 modelo = random.choice(catalogo[marca])
57 letras = fake.lexify(text='????').upper()
58 numeros = random.randint(10, 99)
59 patente = f"{letras[:2]}{letras[2:]}-{numeros}"
60
61 # 80% de vehículos tienen chofer asignado, 20% quedan en patio (NULL)
62 piloto_asignado = random.choice(pilotos_creados) if random.random() > 0.2 else None
63 estado = 'EN_RUTA' if piloto_asignado else random.choice(['DISPONIBLE', 'MANTENIMIENTO'])
64
65 Vehiculo.objects.create(
66 patente=patente,
67 marca=marca,
68 modelo=modelo,
69 anio=random.randint(2018, 2025),
70 kilometraje=random.randint(2000, 180000),
71 estado=estado,
72 piloto_asignado=piloto_asignado
73 )
74
75 messages.success(request, f"¡Éxito! Se generaron {cantidad} pilotos y {cantidad} vehículos correctamente.")
76 return redirect('flota:listar_vehiculos')
77
78 return render(request, 'flota/generar_fake.html')

7.3 Rutas (`flota/urls.py`)

PYTHON
1# flota/urls.py
2from django.urls import path
3from . import views
4
5app_name = 'flota'
6
7urlpatterns = [
8 path('vehiculos/', views.listar_vehiculos_view, name='listar_vehiculos'),
9 path('fake-data/', views.generar_flota_fake_view, name='generar_fake'),
10]

7.4 Plantilla HTML (`templates/flota/generar_fake.html`)

HTML
1<!-- templates/flota/generar_fake.html -->
2{% extends "base.html" %}
3
4{% block content %}
5<div class="container my-5 max-w-lg mx-auto">
6 <div class="card p-4 shadow-lg border-0 rounded-4">
7 <h2 class="h4 font-bold mb-3">⚡ Generador de Flota con Faker</h2>
8 <p class="text-muted text-sm mb-4">
9 Esta herramienta puebla automáticamente la base de datos con conductores, patentes y vehículos realistas.
10 </p>
11
12 <form method="POST">
13 {% csrf_token %}
14 <div class="mb-3">
15 <label for="cantidad" class="form-label font-semibold">Cantidad de registros:</label>
16 <select name="cantidad" id="cantidad" class="form-select">
17 <option value="5">5 vehículos y pilotos</option>
18 <option value="15" selected>15 vehículos y pilotos (Recomendado)</option>
19 <option value="50">50 vehículos y pilotos (Carga masiva)</option>
20 </select>
21 </div>
22
23 <button type="submit" class="btn btn-warning w-100 font-bold py-2">
24 🚀 Ejecutar Controlador y Sembrar Flota
25 </button>
26 </form>
27 </div>
28</div>
29{% endblock %}

8. Consultas ORM Sobre la Flota Generada

Una vez poblada la base de datos con Faker, puedes aprovechar el ORM de Django para realizar consultas eficientes sin incurrir en el problema N+1:

PYTHON
1# 1. Traer vehículos y sus pilotos con INNER/LEFT JOIN en una sola consulta SQL
2vehiculos = Vehiculo.objects.select_related('piloto_asignado').all()
3
4# 2. Filtrar camiones disponibles en patio sin chofer asignado
5camiones_libres = Vehiculo.objects.filter(
6 estado=Vehiculo.Estado.DISPONIBLE,
7 piloto_asignado__isnull=True
8).order_by('-anio')
9
10# 3. Métricas agregadas de toda la flota
11from django.db.models import Avg, Max, Count
12stats = Vehiculo.objects.aggregate(
13 promedio_km=Avg('kilometraje'),
14 maximo_km=Max('kilometraje'),
15 total=Count('id')
16)

9. Ponte a Prueba

Comprueba que comprendes el flujo de migraciones, la política SET_NULL y la implementación de controladores con Faker y transacciones atómicas.

Ko-fi
Donaciones
Apoyá cafeycodigo con un café en Ko-fi. Colaboradores: insignia, muro y zona exclusiva.