Mapeo Objeto-Relacional con Django: Fundamentos y Patrones Avanzados

Fundamentos del ORM en Django

El patrón ORM (Object-Relational Mapping) establece una capa de abstración entre los modelos de datos y el sistema de gestión de bases de datos. Esta arquitectura permite que la definición de entidades sea independiente del motor de almacenamiento, facilitando la migración entre diferentes sistemas de bases de datos mediante ajustes configurables.

Operaciones Fundamentales del Administrador de Consultas

Django proporciona un conjunto robusto de métodos para la recuperación y manipulación de registros:

Conjuntos de Resultados

  • all() — Recupera todos los registros de una entidad
  • filter() — Filtra registros que cumplen las condiciones especificadas
  • exclude() — Omite registros que coinciden con los criterios
  • order_by() — Ordena los resultados por los campos indicados
  • reverse() — Invierte el ordenamiento del conjunto
  • distinct() — Elimina duplicados del resultado
  • values() — Retorna diccionarios en lugar de instancias
  • values_list() — Devuelve tuplas con los valores solicitados

Recuperación de Instancias Individuales

  • get() — Obtiene un único registro que cumpla la condición
  • first() — Primer elemento del conjunto
  • last() — Último elemento del conjunto

Agregaciones y Verificación

  • count() — Cuenta registros (retorna entero)
  • exists() — Verifica existencia (retorna booleano)

Consultas con Lookups de Campo

El sistema de lookups de Django utiliza el patrón campo__modificador para realizar comparaciones avanzadas:

from miapp.models import Libro

# Comparaciones de rango
Libro.objects.filter(costo__in=[150, 250, 350])
Libro.objects.filter(costo__gt=150)      # mayor que
Libro.objects.filter(costo__lt=150)      # menor que
Libro.objects.filter(costo__range=(150, 300))  # entre valores

# Búsquedas de texto
Libro.objects.filter(nombre__contains="django")       # sensible a mayúsculas
Libro.objects.filter(nombre__icontains="django")      # insensible a mayúscula
Libro.objects.filter(nombre__startswith="Django")     # comienza con

# Filtros temporales
Libro.objects.filter(fecha_lanzamiento__year=2023)
Libro.objects.filter(fecha_lanzamiento__year__gt=2023)

Resumen de Modificadores de Campo

Modificador Función
__gt, __lt Mayor/menor que
__gte, __lte Mayor/menor o igual que
__in Pertenencia a lista
__range Dentro de intervalo
__contains, __icontains Contenido de cadena
__startswith, __istartswith Prefijo
__endswith, __iendswith Sufijo
__isnull Verificación de nulo
__year, __month, __day Extraccción de componentes de fecha

Definición de Relaciones entre Entidades

Relaciones de Clave Foránea

class Editorial(models.Model):
    identificador = models.AutoField(primary_key=True)
    razon_social = models.CharField(max_length=128)

class Ejemplar(models.Model):
    titulo = models.CharField(max_length=256)
    editorial = models.ForeignKey(
        Editorial,
        to_field="identificador",
        related_name="publicaciones",
        on_delete=models.CASCADE
    )

Relaciones Muchos a Muchos

class Escritor(models.Model):
    nombre_completo = models.CharField(max_length=128)

class Obra(models.Model):
    denominacion = models.CharField(max_length=256)
    escritores = models.ManyToManyField(Escritor)

# El campo escritores no existe como columna real;
# Django genera una tabla intermedia para gestionar la relación

Para manipular las asociaciones, Django expone un gestor de relaciones:

autor = Escritor.objects.get(id=1)
autor.obra_set.all()                    # consulta inversa
autor.obra_set.add(obra_nueva)          # añadir relación
autor.obra_set.remove(obra_vieja)       # eliminar relación
autor.obra_set.clear()                  # eliminar todas las relaciones
autor.obra_set.set([obra1, obra2])     # reemplazar conjunto
autor.obra_set.create(denominacion="Nuevo Título")  # crear y vincular

Estrategias de Navegación entre Tablas

Consulta por Objetos (Subconsultas SQL)

# Dirección directa: usar el nombre del campo ForeignKey
ejemplar = Ejemplar.objects.get(id=5)
ejemplar.editorial.razon_social

# Dirección inversa: usar related_name o nombre_tabla_set
editorial = Editorial.objects.get(id=1)
editorial.publicaciones.all()           # usando related_name
# editorial.ejemplar_set.all()          # usando el valor por defecto

Consulta por Lookups Cruzados (JOIN en SQL)

# Dirección directa
Ejemplar.objects.filter(denominacion="Python Avanzado").values("editorial__razon_social")

# Dirección inversa
Escritor.objects.filter(nombre_completo__startswith="G").values("obra__denominacion")

Funciones de Agregación y Agrupamiento

Agregaciones Globales

from django.db.models import Avg, Max, Min, Sum, Count

# Promedio de precios
Ejemplar.objects.aggregate(promedio=Avg("costo"))

# Múltiples agregaciones simultáneas
Ejemplar.objects.aggregate(
    mayor=Max("costo"),
    menor=Min("costo"),
    total=Sum("unidades")
)

Agrupamiento con Anotaciones

<p>La cláusula <code>annotate()</code> permite agrupar registros y calcular métricas por grupo, combinando internamente las tablas relacionadas mediante JOIN.</p>
<code># Promedio salarial por departamento
Empleado.objects.values("departamento").annotate(salario_medio=Avg("salario"))

# Autores con precio máximo de sus obras
Escritor.objects.annotate(
    precio_maximo=Max("obra__costo")
).values("nombre_completo", "precio_maximo")

# Autores nacidos después de 1996 con precio máximo de publicaciones
Escritor.objects.filter(
    fecha_nacimiento__year__gt=1996
).annotate(
    precio_maximo=Max("obra__costo")
).values("nombre_completo", "precio_maximo")

# Libros con más de un autor
Obra.objects.annotate(
    total_escritores=Count("escritores")
).filter(total_escritores__gt=1).values("denominacion", "total_escritores")
</code>

<h2>Comparaciones entre Campos con F()</h2>
<p>La clase <code>F</code> permite referenciar el valor actual de un campo en la base de datos sin cargarlo en memoria:</p>
<code>from django.db.models import F

# Filtrar donde comentarios < favoritos
Ejemplar.objects.filter(total_comentarios__lt=F('total_favoritos'))

# Actualización atómica: incrementar precio un 15%
Ejemplar.objects.all().update(costo=F("costo") * 1.15)
</code>

<h2>Combinación Condicional de Criterios con Q()</h2>
<p>Los objetos <code>Q</code> habilitan la construcción de consultas con operadores lógicos complejos:</p>
<code>from django.db.models import Q

# Condición OR
Obra.objects.filter(
    Q(escritores__nombre_completo="García Márquez") | 
    Q(escritores__nombre_completo="Borges")
)

# Negación con ~
Obra.objects.filter(
    Q(escritores__nombre_completo="García Márquez") & 
    ~Q(fecha_publicacion__year=2017)
).values_list("denominacion", flat=True)

# Combinación de Q y parámetros nombrados
Obra.objects.filter(
    Q(fecha_publicacion__year=2023) | Q(fecha_publicacion__year=2024),
    denominacion__icontains="python"
)
</code>

<h2>Tablas Intermedias Personalizadas</h2>
<p>Cuando la relación muchos a muchos requiere atributos adicionales, se define un modelo explícito:</p>
<code>class Estudiante(models.Model):
    nombre = models.CharField(max_length=64)
    asignaturas = models.ManyToManyField(
        "Asignatura",
        through="Matricula",
        related_name="inscritos"
    )

class Asignatura(models.Model):
    titulo = models.CharField(max_length=128)

class Matricula(models.Model):
    estudiante = models.ForeignKey(Estudiante, on_delete=models.CASCADE)
    asignatura = models.ForeignKey(Asignatura, on_delete=models.CASCADE)
    calificacion = models.DecimalField(max_digits=4, decimal_places=2)
    fecha_inscripcion = models.DateField(auto_now_add=True)
</code>

<h2>Configuración del Entorno Django</h2>

<h3>Paso 1: Definición de Modelos</h3>
<code>from django.db import models

class Producto(models.Model):
    codigo = models.AutoField(primary_key=True)
    descripcion = models.CharField(max_length=64, verbose_name="Nombre del producto")
    activo = models.BooleanField(default=True)
    fecha_registro = models.DateField(auto_now_add=True)
    valor = models.DecimalField(max_digits=10, decimal_places=2)
    proveedor = models.CharField(max_length=64)
    
    class Meta:
        app_label = "inventario"
        db_table = "productos_catalogo"
        unique_together = ["descripcion", "proveedor"]
        verbose_name = "Producto comercial"
        ordering = ["valor"]
    
    def __str__(self):
        return self.descripcion
</code>

<h3>Paso 2: Configuración de Conexiones</h3>
<code>DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'almacen_db',
        'USER': 'admin_db',
        'PASSWORD': 'clave_segura',
        'HOST': '192.168.1.50',
        'PORT': '3306',
    },
    'reportes': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'analytics',
        'USER': 'lector',
        'PASSWORD': 'lectura',
        'HOST': '192.168.1.51',
        'PORT': '5432',
    }
}
</code>

<h3>Paso 3: Adaptador para Python 3</h3>
<code># archivo __init__.py del proyecto
import pymysql
pymysql.install_as_MySQLdb()
</code>

<h3>Paso 4: Generación de Esquema</h3>
<code>python manage.py makemigrations
python manage.py migrate
</code>

<h3>Paso 5: Registro de Consultas SQL</h3>
<code>LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'consola': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['consola'],
            'level': 'DEBUG',
            'propagate': True,
        },
    }
}
</code>

<h2>Inserción de Registros</h2>
<code># Método 1: Creación directa
articulo = Producto.objects.create(
    descripcion="Teclado mecánico",
    activo=True,
    valor=Decimal("899.99"),
    proveedor="Periféricos SA"
)

# Método 2: Instanciación y persistencia
articulo = Producto(
    descripcion="Mouse inalámbrico",
    activo=True,
    valor=Decimal("459.50"),
    proveedor="Periféricos SA"
)
articulo.save()
</code>

<h2>Tipos de Campos y Parámetros</h2>
Campo Uso Parámetros Destacados
CharField Cadenas cortas max_length (obligatorio)
TextField Texto largo —
IntegerField Enteros —
FloatField Decimales —
DecimalField Moneda/precisión max_digits, decimal_places
AutoField Clave primaria secuencial primary_key=True
BooleanField Valores verdadero/falso —
DateField Fechas auto_now, auto_now_add
DateTimeField Fecha y hora auto_now, auto_now_add
EmailField Correos electrónicos Validación automática
ImageField Imágenes upload_to, height_field, width_field

Parámetros Universales de Campos

  • null=True: Permite valores NULL en la base de datos
  • blank=True: Permite valores vacíos en formularios de administración
  • default=valor: Establece valor por defecto
  • primary_key=True: Designa como clave primaria
  • unique=True: Restricción de unicidad
  • choices=lista_tuplas: Opciones para desplegables

Etiquetas: Django ORM Database Python SQL

Publicado el 10-27 22:33