EXCEPCIONES Y REPOSITORIO EN SQLITE¶
- Módulo: PD4 - CEPY
- Unidad: UT3 — Persistencia con bases de datos
- Prerrequisitos: apuntes UT3-1, clases Python, herencia, excepciones básicas
1. El gestor de contexto with conn:¶
Por qué usarlo¶
En los apuntes anteriores llamábamos a commit() manualmente al terminar cada operación para que los cambios pendientes se vuelquen en disco. Pero nunca gestionamos qué ocurre si algo falla. Ahora que vamos a trabajar con excepciones necesitamos también deshacer la transacción en caso de error.
El gestor de contexto with conn: nos evita tener que hacerlo a mano:
- Si el bloque termina sin errores → se ejecuta
commit()automático. Cambios se escriben en disco. Se procesa transacción - Si el bloque termina con una excepción → se ejecuta
rollback()de forma automática, deshaciendo los cambios en memoria. Se deshace transacción
Patrón recomendado¶
El siguiente patrón a la hora de ejecutar desde Python consultas en la BBDD evita gestionar manualmente las transacciones y reduce el riesgo de dejar cambios "a medias".
No cierra la conexión
with conn: gestiona la transacción, no la conexión. Hay que cerrar la conexión con conn.close() en el bloque finally.
sqlite3.IntegrityErrores uno de los tipos de excepción de SQLite — los veremos en detalle en la sección siguiente.
si el fichero no existe se crea
Recuerda que si al intentar abrir el fichero de la base de datos este no existe, se crea automáticamente. Si queremos comprobar que el fichero existe antes de abrirlo es recuerda que el módulos Path incluye el método exists() para verificar si un fichero existe:
Comparado con el patrón manual de UT3-1, desaparecen las llamadas explícitas a commit() y rollback() — el código queda más limpio y menos propenso a errores.
Patrón avanzado¶
Si no queremos tener que cerrar manualmente la conexión tendríamos que añadir un with para la propia conexión:
De esta forma tenemos dos context managers anidados, cada uno con su responsabilidad:
with sqlite3.connect(...) as conn:→ gestiona la conexión (la cierra al salir)with conn:→ gestiona la transacción (commit o rollback)
Ahora no necesitamos el finally: conn.close().
En el lab de la unidad no usaremos este patrón sino el que vimos en el apartado anterior
2. Manejo de excepciones en SQLite¶
Jerarquía de excepciones¶
El módulo sqlite3 define su propia jerarquía de excepciones, todas derivadas de sqlite3.Error:
sqlite3.Error
└── sqlite3.DatabaseError
├── sqlite3.IntegrityError ← violación de restricciones (PK, FK, NOT NULL, UNIQUE)
├── sqlite3.OperationalError ← errores del motor (tabla inexistente, SQL incorrecto…)
├── sqlite3.ProgrammingError ← errores de uso de la API (parámetros incorrectos…)
├── sqlite3.DataError ← datos fuera de rango o tipo incorrecto
└── sqlite3.NotSupportedError ← operación no soportada por el motor
En la práctica, las dos que encontrarás con más frecuencia son IntegrityError y OperationalError.
IntegrityError — violación de restricciones¶
Se lanza cuando se viola una restricción de la tabla: PRIMARY KEY, NOT NULL, UNIQUE o FOREIGN KEY.
| Causa | Excepción | Ejemplo |
|---|---|
| Clave primaria duplicada o campos únicos | UNIQUE constraint failed | Dos productos con el mismo codigo |
| Campo NOT NULL con None | NOT NULL constraint failed | None en una columna obligatoria |
| Clave foránea sin referencia | FOREIGN KEY constraint failed | codigo en descuentos que no existe en productos |
OperationalError — errores del motor¶
Se lanza ante problemas del motor SQLite: tabla inexistente, SQL incorrecto, o intentar crear una tabla que ya existe.
| Causa | Ejemplo |
|---|---|
| Tabla inexistente | SELECT * FROM tabla_que_no_existe |
| SQL con sintaxis incorrecta | SELEC * FROM productos |
Tabla ya existente sin IF NOT EXISTS |
CREATE TABLE productos (...) cuando ya existe |
IF NOT EXIST al crear tabla
Para CREATE TABLE deberíamos ejecutar la consulta con IF NOT EXISTS, en lugar de capturar la excepción:
ProgrammingError — uso incorrecto de la API¶
Se lanza cuando el número de parámetros ? no coincide con los valores proporcionados, o se usa la conexión después de cerrarla.
Resumen de excepciones¶
| Excepción | Cuándo se lanza | Qué hacer |
|---|---|---|
sqlite3.IntegrityError |
PK duplicada, NOT NULL, FK sin referencia | Informar al usuario — with ya hizo el rollback |
sqlite3.OperationalError |
Tabla inexistente, SQL incorrecto, tabla ya existente | Revisar la consulta o el esquema |
sqlite3.ProgrammingError |
Número de ? incorrecto, conexión cerrada |
Corregir el código |
sqlite3.Error |
Cualquier error de sqlite3 | Solo como último recurso |
Captura siempre la excepción más concreta primero
Ordena los except de más específico a más general. Si pones sqlite3.Error antes que sqlite3.IntegrityError, el bloque genérico lo interceptará todo y perderás información sobre la causa del fallo.
3. Excepciones propias de dominio¶
Por qué crear excepciones propias¶
Cuando el repositorio captura un sqlite3.IntegrityError y lo deja propagarse, el menú o la capa de aplicación recibe una excepción de infraestructura que no debería conocer. El menú no debería saber nada de SQLite: solo debería saber de productos, precios y stocks.
La solución es que el repositorio capture las excepciones de SQLite y las transforme en excepciones propias del dominio:
El menú captura ProductoYaExisteError y muestra un mensaje legible. Nunca ve sqlite3.IntegrityError.
Definir excepciones propias¶
Las excepciones propias son clases que heredan de Exception. La convención es crear una excepción base del módulo y derivar de ella las específicas:
Herencia en las excepciones propias
Que todas hereden de ErrorRepositorio permite al código que llama capturar cualquier error de persistencia con un solo except ErrorRepositorio, o capturar errores concretos con except ProductoYaExisteError. La misma lógica que con la jerarquía de sqlite3.
Usar las excepciones propias¶
Ahora el código que llama a guardar() solo necesita conocer ProductoYaExisteError:
4. El repositorio como frontera¶
Las capas de la aplicación¶
Una aplicación bien estructurada separa el código en capas con responsabilidades distintas. Cada capa solo conoce la que tiene inmediatamente debajo:
| Capa | Responsabilidad |
|---|---|
| Menú / Interfaz de usuario | Captura excepciones de dominio |
| Dominio (Item, Maquina…) | Lógica de negocio, sin SQLite |
| Repositorio | ← FRONTERA: traduce SQLite ↔ dominio |
| SQLite / Base de datos | Lanza excepciones sqlite3.* |
El repositorio es la única capa que importa sqlite3. Todo lo que está por encima solo ve objetos de dominio y excepciones de dominio.
Ejemplo completo: repositorio con excepciones propias¶
Cómo queda el menú¶
El menú nunca importa sqlite3. Solo captura excepciones de dominio:
Ventaja de esta separación
Si en el futuro cambias SQLite por PostgreSQL o por un fichero JSON, solo tienes que reescribir el repositorio. El menú, el dominio y los tests de dominio no cambian porque nunca supieron que existía SQLite.