Saltar al contenido

# sql-desde-python

SQL desde Python

Volver a la portada del curso
Notebook

Ejemplos 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 SQL y una lista INDICES reú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.Row y los índices. Sus métodos tienen nombres de negocio: registrar_pedido, ventas_por_categoria, pedidos_de.
  • Excepciones propias. ErrorTienda y su subclase StockInsuficiente llevan 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

  1. Parámetros siempre; nunca f-strings con valores externos.
  2. Una transacción with con: por cada operación que escribe.
  3. PRAGMA foreign_keys = ON en el único lugar donde se abre la conexión.
  4. Captura excepciones específicas; traduce las que entiendes y deja subir las demás.
  5. El SQL en constantes con nombre, separado de la lógica.
  6. Índices justificados con EXPLAIN QUERY PLAN.
  7. Solo las columnas necesarias en cada SELECT.
  8. Cerrar la conexión con closing o al terminar el programa.
  9. Pruebas con assert sobre :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.db puede 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.

Recursos