# sql-desde-python
SQL desde Python
Volver a la portada del cursoEjemplos de Buenas Prácticas
Sección 6 · Proyecto Práctico Final
Ejemplos de buenas prácticas
A lo largo del curso cada técnica apareció por separado: parámetros, transacciones, manejo de errores, índices. En una aplicación real todas trabajan juntas, y el orden en que se organizan decide si el código se puede mantener. Esta guía acompaña al notebook buenas-practicas, que parte de una función con cuatro problemas y la reescribe como una pequeña capa de acceso a datos para PyShop. Es la base del proyecto final.
1. El punto de partida: lo que conviene evitar
La primera celda registra un pedido con este estilo: arma el SQL con f-strings, ejecuta tres sentencias sin transacción, envuelve todo en except Exception: pass y nunca cierra la conexión. Se le piden 10 «Juego de cuchillos» cuando hay 9. El UPDATE del stock falla por el CHECK, el error desaparece y el notebook muestra el resultado: el pedido 2001 quedó insertado, con una línea de detalle a precio 0, y la transacción sigue abierta. El próximo commit de cualquier otra operación guardaría ese pedido imposible, y nadie se enteraría.
2. Una clase que envuelve la conexión
La versión corregida se organiza en tres piezas:
- El SQL en constantes con nombre. Un diccionario
SQLy una listaINDICESreúnen todas las sentencias. Se leen, se revisan y se prueban en un solo lugar. - Una clase
TiendaDB. Su constructor configura la conexión una vez:PRAGMA foreign_keys = ON,row_factory = sqlite3.Rowy los índices. Sus métodos tienen nombres de negocio:registrar_pedido,ventas_por_categoria,pedidos_de. - Excepciones propias.
ErrorTienday su subclaseStockInsuficientellevan mensajes que se pueden mostrar al usuario.
class TiendaDB:
def __init__(self, con):
self.con = con
self.con.execute("PRAGMA foreign_keys = ON")
self.con.row_factory = sqlite3.Row
with self.con:
for sql in INDICES:
self.con.execute(sql)
def pedidos_de(self, cliente):
filas = self.con.execute(SQL["pedidos_cliente"], (cliente,))
return [dict(r) for r in filas]
Quien usa la clase recibe listas y diccionarios de Python; no necesita saber SQL.
3. Escrituras: validar, transacción y traducir errores
registrar_pedido sigue un orden fijo: rechaza un pedido vacío, abre with self.con:, inserta el pedido con RETURNING id, y por cada artículo lee precio y stock, valida y escribe. Si el stock no alcanza, lanza StockInsuficiente dentro de la transacción y el rollback deshace también el pedido ya insertado. Si la base responde con un IntegrityError de clave foránea, lo traduce a «No existe el cliente 9999» con raise ... from e; cualquier otro error sube sin cambios. En el notebook, los cuatro intentos fallidos (sin stock, cliente inexistente, producto inexistente y pedido vacío) dejan la base con los mismos 2001 pedidos.
La conexión se cierra con contextlib.closing. Recuerda que with con: confirma o deshace, pero no cierra:
from contextlib import closing
with closing(crear_bd()) as con:
tienda = TiendaDB(con)
pid = tienda.registrar_pedido(1, [(1, 2), (2, 1)])
# aquí la conexión ya está cerrada
4. Índices con justificación
TiendaDB crea dos índices porque sus consultas los usan: pedidos(cliente_id) para los pedidos de un cliente y detalle(producto_id) para las ventas de un producto. El notebook lo comprueba con EXPLAIN QUERY PLAN: ambas consultas aparecen como SEARCH ... USING INDEX. La de pedidos muestra además USE TEMP B-TREE FOR ORDER BY: SQLite ordena en memoria los pocos pedidos del cliente, un costo mínimo para tan pocas filas.
5. Pruebas sobre :memory:
Una base en memoria se crea en milisegundos y desaparece al cerrar, así que cada prueba empieza desde cero y nunca toca datos reales. El notebook incluye tres pruebas con assert: un pedido válido descuenta el stock; un pedido imposible no deja rastro (2000 pedidos, stock del producto 1 en 55 y in_transaction en False), y el reporte de ventas suma 2,843,708 con Electrónica en primer lugar. En un proyecto, estas funciones van en un archivo test_*.py y se ejecutan con pytest, que entiende los assert sin cambios.
6. Lista de revisión
- Parámetros siempre; nunca f-strings con valores externos.
- Una transacción
with con:por cada operación que escribe. PRAGMA foreign_keys = ONen el único lugar donde se abre la conexión.- Captura excepciones específicas; traduce las que entiendes y deja subir las demás.
- El SQL en constantes con nombre, separado de la lógica.
- Índices justificados con
EXPLAIN QUERY PLAN. - Solo las columnas necesarias en cada
SELECT. - Cerrar la conexión con
closingo al terminar el programa. - Pruebas con
assertsobre:memory:, incluido el caso «todo o nada».
Trampas comunes
- Una clase que lo hace todo. La capa de datos guarda y lee; las reglas de presentación (menús, formato) van fuera.
- Abrir una conexión por consulta. Abre una al inicio y pásala; abrir y cerrar en cada llamada desperdicia tiempo y complica las transacciones.
- Probar contra la base real. Una prueba que escribe en
pyshop.dbpuede dejar datos de prueba mezclados con los reales.
Para terminar, resuelve los ejercicios: agrega a la clase registrar_producto, que traduce un nombre repetido o un precio no positivo a ErrorTienda, y poco_stock(limite). Son dos de las operaciones que pide el proyecto final.