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.
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