Introducción a SQLAlchemy ORM
Para interactuar con bases de datos relacionales en el ecosistema Python, SQLAlchemy se destaca como una herramienta fundamental. Este framework ORM (Object-Relational Mapping) permite a los desarrolladores manipular datos utilizando clases y objetos en lugar de escribir sentencias SQL crudas, ofreciendo una capa de abstracción robusta y flexible.
Preparación del Entorno
El primer paso consiste en instalar la librería principle mediante el gestor de paquetes. Dependiendo del sistema de gestión de bases de datos (SGBD) que se vaya a utilizar, será necesario instalar también los controladores específicos.
pip install sqlalchemy
Para conectividad con motores específicos, se requieren dependencias adicionales:
# Controlador para PostgreSQL
pip install psycopg2-binary
# Controlador para MySQL
pip install mysql-connector-python
# SQLite no requiere instalación extra (viene en la stdlib)
Componentes Principales
La arquitectura de SQLAlchemy se basa en varios pilares clave:
- Motor (Engine): Gestiona la conexión pool y la comunicaicón directa con la base de datos.
- Sesión (Session): Actúa como una zona de operaciones (staging area) donde se mantienen los objetos antes de persistirlos.
- Modelo (Model): Clase Python que representa una tabla en la base de datos.
- Consulta (Query): Objeto encargado de construir y ejecutar operaciones de lectura.
Estableciendo la Conexión
Se inicia configurando el motor de base de datos. La cadena de conexión define el dialecto y las credenciales. Posteriormente, se factory una sesión para manejar las transacciones.
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Configuración del motor (ejemplo SQLite)
motor = create_engine('sqlite:///inventario.db', echo=False)
# Ejemplo para PostgreSQL
# motor = create_engine('postgresql://usuario:clave@localhost:5432/db_ventas')
# Fabrica de sesiones
FabricaSesion = sessionmaker(autocommit=False, autoflush=False, bind=motor)
# Instancia de sesión activa
sesion = FabricaSesion()
Definición de Modelos de Datos
Los modelos se definen heredando de una base declarativa. Aquí se establecen las columnas, tipos de datos y las relaciones entre tablas.
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, declarative_base
Base = declarative_base()
class Cliente(Base):
__tablename__ = 'clientes'
identificador = Column(Integer, primary_key=True, index=True)
nombre_completo = Column(String(100), nullable=False)
correo_electronico = Column(String(150), unique=True, index=True)
# Relación uno a muchos con pedidos
pedidos = relationship("Pedido", back_populates="propietario")
class Pedido(Base):
__tablename__ = 'pedidos'
id_pedido = Column(Integer, primary_key=True, index=True)
descripcion = Column(String(200), nullable=False)
monto = Column(Integer)
cliente_id = Column(Integer, ForeignKey('clientes.identificador'))
# Relación muchos a uno con cliente
propietario = relationship("Cliente", back_populates="pedidos")
# Relación muchos a muchos con categorías
categorias = relationship("Categoria", secondary="pedido_categoria", back_populates="pedidos")
class Categoria(Base):
__tablename__ = 'categorias'
id_cat = Column(Integer, primary_key=True, index=True)
nombre = Column(String(50), unique=True, nullable=False)
pedidos = relationship("Pedido", secondary="pedido_categoria", back_populates="categorias")
# Tabla intermedia para relación muchos a muchos
class PedidoCategoria(Base):
__tablename__ = 'pedido_categoria'
pedido_id = Column(Integer, ForeignKey('pedidos.id_pedido'), primary_key=True)
categoria_id = Column(Integer, ForeignKey('categorias.id_cat'), primary_key=True)
Generación de Esquema
Una vez definidos los modelos, SQLAlchemy puede crear automáticamente las tablas en la base de datos conectada.
# Crear tablas según los modelos definidos
Base.metadata.create_all(bind=motor)
# Para eliminar todas las tablas (uso cuidadoso)
# Base.metadata.drop_all(bind=motor)
Operaciones CRUD Básicas
Insertar Registros
Los objetos se instancian como clases normales y se añaden a la sesión antes de confirmar los cambios.
# Creación individual
nuevo_cliente = Cliente(nombre_completo="Ana Gómez", correo_electronico="ana@empresa.com")
sesion.add(nuevo_cliente)
sesion.commit()
# Inserción masiva
sesion.add_all([
Cliente(nombre_completo="Luis Pérez", correo_electronico="luis@empresa.com"),
Cliente(nombre_completo="Carla Díaz", correo_electronico="carla@empresa.com")
])
sesion.commit()
Consultar Información
La recuperación de datos se realiza mediante objetos de consulta que permiten filtrar y navegar por los registros.
# Obtener toda la lista
clientes = sesion.query(Cliente).all()
# Obtener un único registro
primer_cliente = sesion.query(Cliente).first()
# Búsqueda por clave primaria
cliente_especifico = sesion.query(Cliente).get(1)
Actualizar Datos
Se modifica el atributo del objeto cargado en memoria y se persiste el cambio.
# Modificación directa
cliente = sesion.query(Cliente).get(1)
cliente.nombre_completo = "Ana Gómez Rodríguez"
sesion.commit()
# Actualización bulk (más eficiente para muchos registros)
sesion.query(Cliente).filter(Cliente.nombre_completo.like("Luis%")).update(
{"nombre_completo": "Luis Actualizado"},
synchronize_session=False
)
sesion.commit()
Eliminar Registros
Los objetos pueden ser removidos de la base de datos mediante la sesión.
# Borrado de instancia
cliente = sesion.query(Cliente).get(1)
sesion.delete(cliente)
sesion.commit()
# Borrado bulk
sesion.query(Cliente).filter(Cliente.correo_electronico == "carla@empresa.com").delete(
synchronize_session=False
)
sesion.commit()
Técnicas de Consulta Avanzada
Filtrado y Ordenamiento
Es posible refinar los resultados usando condiciones lógicas, ordenamiento y paginación.
from sqlalchemy import or_
# Filtrado exacto
resultado = sesion.query(Cliente).filter(Cliente.nombre_completo == "Ana Gómez").first()
# Búsqueda parcial (LIKE)
lista = sesion.query(Cliente).filter(Cliente.nombre_completo.like("%Gómez%")).all()
# Operador IN
lista = sesion.query(Cliente).filter(Cliente.nombre_completo.in_(["Ana Gómez", "Luis Pérez"])).all()
# Combinación de condiciones (AND)
lista = sesion.query(Cliente).filter(
Cliente.nombre_completo == "Ana Gómez",
Cliente.correo_electronico.like("%empresa.com")
).all()
# Operador OR
lista = sesion.query(Cliente).filter(
or_(Cliente.nombre_completo == "Ana Gómez", Cliente.nombre_completo == "Luis Pérez")
).all()
# Ordenamiento descendente y limitación
lista = sesion.query(Cliente).order_by(Cliente.identificador.desc()).limit(5).all()
Agregaciones
SQLAlchemy permite ejecutar funciones agregadas directamente sobre las consultas.
from sqlalchemy import func
# Contar registros
total = sesion.query(Cliente).count()
# Agrupamiento y conteo
resultados = sesion.query(
Cliente.nombre_completo,
func.count(Pedido.id_pedido)
).join(Pedido).group_by(Cliente.nombre_completo).all()
# Funciones estadísticas
promedio_id = sesion.query(func.avg(Cliente.identificador)).scalar()
Uniones (Joins)
Para consultar datos relacionados entre tablas, se utilizan uniones explícitas.
# Inner Join
resultados = sesion.query(Cliente, Pedido).join(Pedido).filter(Pedido.descripcion.like("%Urgente%")).all()
# Left Outer Join
resultados = sesion.query(Cliente, Pedido).outerjoin(Pedido).all()
# Join con condición explícita
resultados = sesion.query(Cliente, Pedido).join(Pedido, Cliente.identificador == Pedido.cliente_id).all()
Manejo de Relaciones
Las relaciones definidas en los modelos permiten navegar entre objetos relacionados sin escribir joins manualmente.
# Creación de objetos vinculados
cliente = Cliente(nombre_completo="Roberto Silva", correo_electronico="roberto@web.com")
pedido = Pedido(descripcion="Servicio Consultoría", monto=500, propietario=cliente)
sesion.add(pedido)
sesion.commit()
# Acceso navegacional
print(f"El pedido '{pedido.descripcion}' pertenece a {pedido.propietario.nombre_completo}")
print(f"Pedidos realizados por {cliente.nombre_completo}:")
for p in cliente.pedidos:
print(f" - {p.descripcion}")
# Gestión de relaciones muchos a muchos
cat_tech = Categoria(nombre="Tecnología")
cat_serv = Categoria(nombre="Servicios")
pedido.categorias.append(cat_tech)
pedido.categorias.append(cat_serv)
sesion.commit()
print(f"Categorías del pedido '{pedido.descripcion}':")
for c in pedido.categorias:
print(f" - {c.nombre}")
Control de Transacciones
La integridad de los datos se asegura mediante el manejo explícito de transacciones, confirmando o revirtiendo cambios según el resultado de las operaciones.
# Manejo básico de errores
try:
cliente = Cliente(nombre_completo="Prueba", correo_electronico="prueba@test.com")
sesion.add(cliente)
sesion.commit()
except Exception as e:
sesion.rollback()
print(f"Error en transacción: {e}")
# Función con gestión transaccional
from sqlalchemy.orm import Session
def registrar_cliente(sesion: Session, nombre: str, email: str):
try:
nuevo = Cliente(nombre_completo=nombre, correo_electronico=email)
sesion.add(nuevo)
sesion.commit()
return nuevo
except:
sesion.rollback()
raise
# Savepoints para transacciones anidadas
with sesion.begin_nested():
cliente = Cliente(nombre_completo="Transacción Anidada", correo_electronico="nested@test.com")
sesion.add(cliente)
# Uso explícito de savepoint
punto_guardado = sesion.begin_nested()
try:
cliente = Cliente(nombre_completo="Usuario Savepoint", correo_electronico="sp@test.com")
sesion.add(cliente)
punto_guardado.commit()
except:
punto_guardado.rollback()
Recomendaciones de Implementación
- Ciclo de vida de la sesión: Instanciar una nueva sesión por cada solicitud y cerrarla al finalizar.
- Gestión de excepciones: Implementar bloques try/except para asegurar el rollback en caso de fallos.
- Optimización de cargas: Evitar el problema N+1 utilizando eager loading (joinedload o selectinload) cuando se acceda a relaciones.
- Configuración del Pool: Ajustar el tamaño del pool de conexiones y los tiempos de espera según la carga de la aplicación.
- Validación de datos: Implemantar reglas de validación en la capa de modelo o antes de persistir.
# Patrón de contexto para gestión automática de sesiones
from contextlib import contextmanager
@contextmanager
def obtener_sesion():
db = FabricaSesion()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
# Ejemplo de uso
with obtener_sesion() as db:
cliente = Cliente(nombre_completo="Usuario Contexto", correo_electronico="ctx@test.com")
db.add(cliente)