Asociaciones de modelos

Avanzado
Flask
Flask
Actualizado: 20/06/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

Relación many to one

Las relaciones many-to-one representan uno de los patrones más comunes en el diseño de bases de datos relacionales. En este tipo de asociación, múltiples registros de una tabla pueden estar relacionados con un único registro de otra tabla. Por ejemplo, varios productos pueden pertenecer a una misma categoría, o múltiples pedidos pueden estar asociados a un único cliente.

En SQLAlchemy, implementamos estas relaciones utilizando claves foráneas (ForeignKey) junto con el método relationship() para establecer la conexión bidireccional entre los modelos. Esta combinación nos permite navegar eficientemente entre las entidades relacionadas tanto desde el lado "muchos" como desde el lado "uno".

Implementación básica con ForeignKey

Para establecer una relación many-to-one, comenzamos definiendo la clave foránea en el modelo que representa el lado "muchos" de la relación:

from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

db = SQLAlchemy()

class Categoria(db.Model):
    __tablename__ = 'categorias'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    descripcion = db.Column(db.Text)
    
class Producto(db.Model):
    __tablename__ = 'productos'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(200), nullable=False)
    precio = db.Column(db.Decimal(10, 2), nullable=False)
    categoria_id = db.Column(db.Integer, db.ForeignKey('categorias.id'), nullable=False)

En este ejemplo, el campo categoria_id en el modelo Producto actúa como clave foránea que referencia al id de la tabla categorias. La función db.ForeignKey() establece esta conexión a nivel de base de datos, garantizando la integridad referencial.

Configuración de relationships bidireccionales

Para facilitar la navegación entre modelos relacionados, utilizamos el método relationship() que crea atributos virtuales en nuestros modelos:

class Categoria(db.Model):
    __tablename__ = 'categorias'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    descripcion = db.Column(db.Text)
    
    # Relación hacia los productos
    productos = db.relationship('Producto', backref='categoria', lazy=True)

class Producto(db.Model):
    __tablename__ = 'productos'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(200), nullable=False)
    precio = db.Column(db.Decimal(10, 2), nullable=False)
    categoria_id = db.Column(db.Integer, db.ForeignKey('categorias.id'), nullable=False)

El parámetro backref='categoria' crea automáticamente un atributo categoria en el modelo Producto, permitiendo acceso bidireccional. El parámetro lazy=True indica que los productos relacionados se cargarán bajo demanda cuando se acceda al atributo.

Operaciones de consulta con relaciones

Una vez establecidas las relaciones, podemos realizar consultas que aprovechen estas conexiones de manera eficiente:

# Crear una categoría y productos relacionados
categoria_electronica = Categoria(
    nombre='Electrónica',
    descripcion='Dispositivos electrónicos y gadgets'
)

producto1 = Producto(
    nombre='Smartphone',
    precio=599.99,
    categoria=categoria_electronica  # Asignación directa usando la relación
)

producto2 = Producto(
    nombre='Tablet',
    precio=399.99,
    categoria=categoria_electronica
)

db.session.add_all([categoria_electronica, producto1, producto2])
db.session.commit()

Para consultar datos relacionados, podemos navegar a través de las relaciones establecidas:

# Obtener todos los productos de una categoría específica
categoria = Categoria.query.filter_by(nombre='Electrónica').first()
productos_electronicos = categoria.productos

# Obtener la categoría de un producto específico
producto = Producto.query.filter_by(nombre='Smartphone').first()
categoria_del_producto = producto.categoria

# Consulta con join para optimizar rendimiento
productos_con_categoria = db.session.query(Producto, Categoria)\
    .join(Categoria)\
    .filter(Categoria.nombre == 'Electrónica')\
    .all()

Configuración avanzada de lazy loading

El parámetro lazy en relationship() controla cuándo y cómo se cargan los datos relacionados. Las opciones más utilizadas incluyen:

class Categoria(db.Model):
    __tablename__ = 'categorias'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    
    # Diferentes estrategias de carga
    productos_lazy = db.relationship('Producto', lazy='select')      # Carga bajo demanda
    productos_joined = db.relationship('Producto', lazy='joined')    # Carga con JOIN
    productos_dynamic = db.relationship('Producto', lazy='dynamic')  # Retorna query object

La opción lazy='dynamic' es especialmente útil cuando esperamos grandes cantidades de registros relacionados, ya que retorna un objeto query que podemos filtrar antes de ejecutar:

categoria = Categoria.query.first()
productos_caros = categoria.productos_dynamic.filter(Producto.precio > 500).all()
productos_recientes = categoria.productos_dynamic.order_by(Producto.id.desc()).limit(10).all()

Manejo de integridad referencial

SQLAlchemy nos permite configurar el comportamiento en cascada cuando se eliminan o modifican registros padre:

class Categoria(db.Model):
    __tablename__ = 'categorias'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    
    productos = db.relationship(
        'Producto', 
        backref='categoria',
        cascade='all, delete-orphan',  # Elimina productos huérfanos
        passive_deletes=True           # Delega eliminación a la BD
    )

class Producto(db.Model):
    __tablename__ = 'productos'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(200), nullable=False)
    precio = db.Column(db.Decimal(10, 2), nullable=False)
    categoria_id = db.Column(
        db.Integer, 
        db.ForeignKey('categorias.id', ondelete='CASCADE'), 
        nullable=False
    )

Con esta configuración, cuando eliminamos una categoría, todos sus productos asociados se eliminan automáticamente, manteniendo la consistencia de los datos sin requerir intervención manual.

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

Relación muchos a muchos

Las relaciones many-to-many modelan situaciones donde múltiples registros de una tabla pueden estar asociados con múltiples registros de otra tabla. Un ejemplo típico sería la relación entre usuarios y roles, donde un usuario puede tener varios roles y un rol puede ser asignado a múltiples usuarios.

A diferencia de las relaciones many-to-one, las relaciones muchos a muchos requieren una tabla intermedia (también llamada tabla de asociación) que almacena las conexiones entre ambas entidades. SQLAlchemy nos proporciona dos enfoques para implementar estas relaciones: usando una tabla de asociación simple o creando un modelo intermedio completo.

Implementación con tabla de asociación

Para relaciones muchos a muchos simples, podemos utilizar una tabla de asociación definida con db.Table():

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

# Tabla de asociación para la relación muchos a muchos
usuario_roles = db.Table('usuario_roles',
    db.Column('usuario_id', db.Integer, db.ForeignKey('usuarios.id'), primary_key=True),
    db.Column('rol_id', db.Integer, db.ForeignKey('roles.id'), primary_key=True)
)

class Usuario(db.Model):
    __tablename__ = 'usuarios'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    
    # Relación muchos a muchos
    roles = db.relationship('Rol', secondary=usuario_roles, backref='usuarios')

class Rol(db.Model):
    __tablename__ = 'roles'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(50), unique=True, nullable=False)
    descripcion = db.Column(db.Text)

El parámetro secondary=usuario_roles indica a SQLAlchemy que utilice la tabla intermedia para gestionar la relación. El backref='usuarios' crea automáticamente el atributo usuarios en el modelo Rol.

Operaciones básicas con relaciones muchos a muchos

Una vez definida la relación, podemos manipular las asociaciones de forma intuitiva utilizando los atributos de relación como listas de Python:

# Crear usuarios y roles
admin_rol = Rol(nombre='admin', descripcion='Administrador del sistema')
editor_rol = Rol(nombre='editor', descripcion='Editor de contenido')
viewer_rol = Rol(nombre='viewer', descripcion='Solo lectura')

usuario1 = Usuario(nombre='Ana García', email='ana@ejemplo.com')
usuario2 = Usuario(nombre='Carlos López', email='carlos@ejemplo.com')

# Asignar roles a usuarios
usuario1.roles.append(admin_rol)
usuario1.roles.append(editor_rol)

usuario2.roles.extend([editor_rol, viewer_rol])

db.session.add_all([admin_rol, editor_rol, viewer_rol, usuario1, usuario2])
db.session.commit()

Para consultar y modificar las relaciones existentes:

# Obtener todos los roles de un usuario
usuario = Usuario.query.filter_by(email='ana@ejemplo.com').first()
roles_del_usuario = usuario.roles

# Obtener todos los usuarios con un rol específico
rol_admin = Rol.query.filter_by(nombre='admin').first()
usuarios_admin = rol_admin.usuarios

# Eliminar una relación específica
usuario.roles.remove(editor_rol)
db.session.commit()

# Verificar si existe una relación
tiene_rol_admin = admin_rol in usuario.roles

Consultas avanzadas con joins

Para realizar consultas más complejas que involucren ambas tablas, podemos utilizar joins explícitos:

# Usuarios que tienen el rol de 'admin'
usuarios_admin = Usuario.query\
    .join(usuario_roles)\
    .join(Rol)\
    .filter(Rol.nombre == 'admin')\
    .all()

# Roles asignados a usuarios cuyo email contiene 'ejemplo.com'
roles_dominio = Rol.query\
    .join(usuario_roles)\
    .join(Usuario)\
    .filter(Usuario.email.like('%ejemplo.com%'))\
    .distinct()\
    .all()

# Contar usuarios por rol
from sqlalchemy import func

estadisticas_roles = db.session.query(
    Rol.nombre,
    func.count(Usuario.id).label('total_usuarios')
)\
.join(usuario_roles)\
.join(Usuario)\
.group_by(Rol.id)\
.all()

Modelo intermedio con atributos adicionales

Cuando necesitamos almacenar información adicional sobre la relación (como fechas de asignación o estados), debemos crear un modelo intermedio completo:

class UsuarioProyecto(db.Model):
    __tablename__ = 'usuario_proyectos'
    
    usuario_id = db.Column(db.Integer, db.ForeignKey('usuarios.id'), primary_key=True)
    proyecto_id = db.Column(db.Integer, db.ForeignKey('proyectos.id'), primary_key=True)
    fecha_asignacion = db.Column(db.DateTime, default=datetime.utcnow)
    rol_proyecto = db.Column(db.String(50), nullable=False)
    activo = db.Column(db.Boolean, default=True)
    
    # Relaciones hacia las entidades principales
    usuario = db.relationship('Usuario', backref='asignaciones_proyecto')
    proyecto = db.relationship('Proyecto', backref='asignaciones_usuario')

class Usuario(db.Model):
    __tablename__ = 'usuarios'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

class Proyecto(db.Model):
    __tablename__ = 'proyectos'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(200), nullable=False)
    descripcion = db.Column(db.Text)

Con este enfoque, podemos trabajar directamente con el modelo intermedio:

# Asignar usuario a proyecto con información adicional
usuario = Usuario.query.first()
proyecto = Proyecto.query.first()

asignacion = UsuarioProyecto(
    usuario=usuario,
    proyecto=proyecto,
    rol_proyecto='desarrollador',
    activo=True
)

db.session.add(asignacion)
db.session.commit()

# Consultar asignaciones con filtros específicos
asignaciones_activas = UsuarioProyecto.query\
    .filter_by(activo=True)\
    .join(Usuario)\
    .filter(Usuario.email.like('%@empresa.com'))\
    .all()

# Obtener proyectos de un usuario con información de la asignación
proyectos_usuario = db.session.query(Proyecto, UsuarioProyecto)\
    .join(UsuarioProyecto)\
    .filter(UsuarioProyecto.usuario_id == usuario.id)\
    .filter(UsuarioProyecto.activo == True)\
    .all()

Optimización de consultas con lazy loading

Al igual que en las relaciones many-to-one, podemos configurar el comportamiento de carga para optimizar el rendimiento:

class Usuario(db.Model):
    __tablename__ = 'usuarios'
    
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    
    # Diferentes estrategias para relaciones muchos a muchos
    roles = db.relationship(
        'Rol', 
        secondary=usuario_roles,
        lazy='subquery',  # Carga con subconsulta separada
        backref=db.backref('usuarios', lazy='dynamic')
    )

La opción lazy='subquery' es especialmente útil para relaciones muchos a muchos, ya que minimiza el número de consultas necesarias para cargar todos los datos relacionados. El backref con lazy='dynamic' permite filtrar eficientemente desde el lado inverso de la relación.

Aprendizajes de esta lección

  • Comprender el concepto y la implementación de relaciones many-to-one en SQLAlchemy.
  • Configurar relaciones bidireccionales usando claves foráneas y el método relationship().
  • Aplicar diferentes estrategias de carga (lazy loading) para optimizar consultas.
  • Implementar relaciones many-to-many mediante tablas de asociación simples y modelos intermedios con atributos adicionales.
  • Realizar consultas y manipulaciones avanzadas sobre asociaciones entre modelos manteniendo la integridad referencial.

Completa Flask y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración