DE OBJETOS PYTHON A BASES DE DATOS RELACIONALES¶
- Módulo: PD4 - CEPY
- Unidad: UT3 — Persistencia con bases de datos
- Prerrequisitos: clases Python, herencia, diccionarios
1. Bases de datos relacionales¶
¿Qué es una base de datos relacional?¶
Una base de datos relacional organiza la información en tablas — estructuras con filas y columnas. Cada tabla representa un tipo de dato concreto (productos, clientes, pedidos). Cada fila es un registro individual y cada columna es un atributo de ese registro.
Por qué se les llama bases de datos relacionales
Se les llama relacionales porque organizan la información en relaciones, que es el término matemático/formal para lo que en la práctica vemos como tablas. Además, esas tablas se relacionan entre sí mediante claves, como una clave primaria y una clave foránea.
La analogía más directa es una hoja de cálculo: las columnas son los campos y cada fila es un elemento de ese tipo.
A diferencia de las hojas de cálculo, las bases de datos relacionales tienen una serie de propiedades o características:
- Tipos: definen qué clase de dato puede guardarse.
- Restricciones: ponen reglas sobre esos datos.
- Integridad: garantiza que los datos se mantengan correctos, válidos y coherentes entre sí. Los datos no se contradicen entre sí y mantienen coherencia dentro de la base de datos
Por ejemplo: En una hoja de cálculo, en la columna “edad” alguien podría escribir 25, veinticinco o incluso dejar -3, y el archivo lo permite.
En una base de datos relacional, puedes definir que:
- “edad” solo sea un número entero → tipo
- no pueda ser negativa → restricción
- que un pedido no pueda existir si no está asociado a un cliente real → integridad (más concretamente, integridad referencial)
Para los ejemplos de estos apuntes usaremos la base de datos de la máquina expendedora, con el siguiente contenido:
── tabla: productos ───────────────────────────
codigo | nombre | precio | cantidad
-------+----------+--------+---------
A1 | Agua | 1.0 | 10
A2 | Papas | 1.5 | 8
D1 | Refresco | 2.5 | 5
── tabla: descuentos ──
codigo | porcentaje
-------+-----------
D1 | 20.0
De diccionario Python a tabla SQL¶
Cuando guardamos objetos Python en memoria, el patrón más habitual es un diccionario con el identificador como clave:
Una tabla SQL hace exactamente lo mismo, pero persiste los datos en disco:
| Concepto Python | Equivalente SQL |
|---|---|
dict / list |
Tabla |
Clase (Item) |
Estructura de la tabla (columnas) |
Objeto (Item("A1", …)) |
Fila |
Atributo (item.nombre) |
Columna |
item.codigo como clave del dict |
PRIMARY KEY |
El diccionario vive en memoria y desaparece al cerrar el programa. La base de datos persiste en un fichero
.dben disco y sobrevive entre ejecuciones.
SQLite — el motor embebido de Python¶
SQLite es un motor de base de datos que almacena toda la información en un único fichero .db. No necesita un servidor separado ni instalación adicional: el módulo sqlite3 viene incluido en la biblioteca estándar de Python.
| Característica | SQLite | MySQL / PostgreSQL |
|---|---|---|
| Instalación | Ninguna (incluido en Python) | Requiere servidor |
| Almacenamiento | Un fichero .db |
Servidor con proceso dedicado |
| Conexión | sqlite3.connect("fichero.db") |
Host, puerto, usuario, contraseña |
| Casos de uso | Desarrollo, apps de escritorio, prototipos, pocos usuarios | Producción, múltiples usuarios |
Usa SQLite para aprender y prototipar
SQLite es la elección ideal cuando empiezas con bases de datos: cero configuración, portabilidad total y el mismo SQL estándar que usarás después con otros motores.
2. El módulo sqlite3¶
Flujo de trabajo: conectar, ejecutar, confirmar, cerrar¶
Toda operación con SQLite sigue el mismo ciclo de cinco pasos. Aprende este patrón de memoria — siempre es el mismo:
La conexión y el cursor¶
sqlite3.connect() abre o crea el fichero de base de datos y devuelve un objeto Connection. Si el fichero expendedora.db no existe, SQLite lo crea vacío en ese momento.
El cursor es el objeto que se usa para envía sentencias SQL a la base de datos y recoge los resultados. Lo creamos siempre con conn.cursor() antes de ejecutar cualquier sentencia. Se llama así porque significa algo parecido a un apuntador que nos permite movernos por los resultados de una consulta fila a fila.
Si ejecutas
sqlite3.connect("expendedora.db")y el fichero no existe, SQLite crea automáticamente un fichero vacío con ese nombre. Si ya existe, lo abre sin borrarlo.
Transacciones y commit()¶
SQLite trabaja con transacciones: los cambios producidos por INSERT, UPDATE o DELETE no se escriben en el fichero hasta que llamas explícitamente a conn.commit(). Puedes agrupar varias operaciones en una sola transacción y confirmarlas todas de golpe.
Mala práctica: olvidar el commit
3. SQL básico: definir y agregar datos iniciales a tablas¶
CREATE TABLE — definir la estructura¶
CREATE TABLE es la sentencia SQL que crea una nueva tabla en la base de datos. Se especifica el nombre de cada columna, su tipo de dato y sus restricciones.
CREATE TABLE IF NOT EXISTS nombre_tabla (
columna1 TIPO RESTRICCIONES,
columna2 TIPO RESTRICCIONES,
...
)
La cláusula IF NOT EXISTS evita un error si la tabla ya existe — imprescindible cuando el script se puede ejecutar varias veces.
En Python, la sentencia SQL se pasa como cadena de texto a cursor.execute():
Tipos de datos: Python vs SQL¶
SQLite trabaja con tres tipos numéricos y uno de texto. La correspondencia con los tipos Python es directa:
| Tipo Python | Tipo SQL | Ejemplo |
|---|---|---|
str |
TEXT |
"A1", "Agua" |
float |
REAL |
1.00, 20.5 |
int |
INTEGER |
10, 5 |
bool |
INTEGER |
1 (True), 0 (False) |
Restricciones: PRIMARY KEY y NOT NULL¶
Una restricción es una regla que la base de datos comprueba automáticamente antes de aceptar cualquier fila.
PRIMARY KEY indica que esa columna identifica de forma única cada fila. No pueden existir dos filas con el mismo valor y nunca puede estar vacía. Es el equivalente a usar el codigo como clave en el diccionario Python.
NOT NULL obliga a que la columna tenga siempre un valor. Si intentas insertar una fila sin ese campo, la base de datos rechaza la operación con un error.
Si intentas insertar dos productos con el mismo
codigo, SQLite lanza una excepciónsqlite3.IntegrityErrorporque viola la restricciónPRIMARY KEY(lo veremos en detalle en la parte 2).Si no usamos la cláusula
IF NOT EXISTSal crear la tabla y esta ya existe, SQLite lanza una excepciónsqlite3.OperationalError(lo veremos en detalle en la parte 2).
INSERT INTO — añadir filas¶
INSERT INTO añade una nueva fila a una tabla existente. Los valores deben respetar el orden y tipo de las columnas definidas en CREATE TABLE.
En Python, cada INSERT es una llamada a cursor.execute():
Cuando los valores vienen de variables Python, usa parámetros ? en lugar de construir la cadena con f-strings o concatenación:
Usa siempre parámetros ? con datos de variables
Los parámetros ? hacen que SQLite gestione automáticamente el escape de los valores, protegiéndote de inyección SQL. Úsalos siempre que los valores provengan de variables, no de literales escritos directamente en el código.
Mala práctica: construir SQL con f-strings
Ejemplo de inyección SQL¶
En la siguiente consulta los datos del usuario se meten directamente dentro del SQL:
codigo = "A1"
nombre = "Agua'); DROP TABLE productos; --" # Datos introducidos por usuario en formulario web.
precio = 1.20
cantidad = 10
sql = f"INSERT INTO productos VALUES ('{codigo}', '{nombre}', {precio}, {cantidad})"
print(sql)
La sentencia SQL quedaría de la forma:
Problema:
- se cierra antes la cadena de nombre
- aparece una nueva orden SQL: DROP TABLE productos
- -- comenta el resto
Esa es la idea de una inyección SQL: convertir un dato en código SQL, en este ejemplo para borrar una tabla.
Versión segura
Con ?, el contenido de nombre se trata solo como dato:
Aunque nombre valga:
SQLite lo guarda como texto dentro del campo, no lo ejecuta. La consulta que se ejecuta sería:
En SQL dos comillas simples seguidas
''significan “una comilla simple dentro del texto”
4. SQL básico: consultar datos¶
SELECT — leer filas¶
SELECT es la sentencia SQL para leer datos de una tabla. SELECT * FROM tabla devuelve todas las columnas de todas las filas. Puedes filtrar con WHERE para obtener solo las filas que cumplan una condición.
SELECT * FROM nombre_tabla
SELECT columna1, columna2 FROM nombre_tabla
SELECT * FROM nombre_tabla WHERE columna = valor
En Python, tras el execute() hay que recoger los resultados con fetchall() o fetchone():
fetchall() y fetchone()¶
Cada fila que devuelve SQLite llega a Python como una tupla. Los elementos mantienen el mismo orden que las columnas definidas en CREATE TABLE, y puedes acceder a ellos por índice.
fetchone()avanza el cursor una posición. Si llamas afetchone()varias veces seguidas sobre el mismoexecute(), obtienes la primera, segunda y tercera fila respectivamente.
5. SQL básico: modificar y eliminar datos¶
UPDATE — modificar filas existentes¶
UPDATE modifica una o varias filas ya existentes. Combina SET para indicar qué campos cambian y WHERE para seleccionar qué filas afectar.
En Python con parámetros ?:
Puedes actualizar varios campos a la vez separándolos con coma en el SET:
Mala práctica: UPDATE sin WHERE
DELETE FROM — eliminar filas¶
DELETE FROM elimina filas de una tabla. El filtro WHERE es fundamental: sin él se eliminan todas las filas de la tabla.
Con claves foráneas activas, hay que eliminar primero la tabla hija antes que la tabla padre; de lo contrario SQLite lanza IntegrityError:
Mala práctica: DELETE sin WHERE
6. Claves primarias, valores nulos y claves foráneas¶
PRIMARY KEY — identificador único¶
La clave primaria (primary key) es la columna que identifica de forma única e irrepetible cada fila de una tabla. Sus reglas son tres:
- No pueden existir dos filas con el mismo valor en la columna clave.
- La clave primaria nunca puede ser
NULL. - Cada tabla tiene exactamente una clave primaria.
En la tabla productos, codigo es la clave primaria: no pueden existir dos productos con el mismo código. Es el equivalente directo a usar codigo como clave de un diccionario Python, donde la clave tampoco puede repetirse.
Python, SQL y valores nulos.¶
NULL se usa en SQL para que indicar que un dato no tiene valor.
No significa:
- ni 0
- ni cadena vacía ''
- ni False.
Significa: valor desconocido, ausente o no aplicable.
Si al crear una tabla especificamos en la definición de un campor NOT NULL significa que esa columna no puede quedarse sin valor. Es decir, al insertar o actualizar una fila, ese campo debe tener algún dato; no se permite NULL.
Cuando en Python “dejas en blanco” un campo en un INSERT o cualquier otra operación que modifique un campo con la restricción NOT NULL, pueden pasar dos cosas distintas:
- Si usas
"", se guarda una cadena vacía. - Si usas
None, se guardaNULLen la base de datos.
Ejemplo:
Aquí nombre vale "": no es NULL, sino texto vacío. Por eso, aunque la columna tenga NOT NULL, no se lanza una excepción.
SQLITE y valores numéricos en blanco
Sqlite es bastante flexible y tampoco lanzará excepción si insertamos "" en un campo numérico. Por eso, en Python conviene validar antes los datos y no enviar "" a columnas numéricas.
En cambio:
Aquí None se convierte en NULL. Si nombre tiene la restricción NOT NULL, la base de datos rechazará la inserción y lanzará una excepción sqlite3.IntegrityError al estarse incumpliendo una restricción de integridad de la tabla (lo veremos en detalle en la parte 2).
FOREIGN KEY — relaciones entre tablas¶
Una clave foránea (foreign key) es una columna que referencia la PRIMARY KEY de otra tabla, estableciendo una relación entre ambas. Garantiza la integridad referencial: no puede existir una fila que apunte a un registro inexistente en la otra tabla.
La línea FOREIGN KEY (codigo) REFERENCES productos(codigo) establece la regla: el valor de descuentos.codigo debe existir previamente en productos.codigo. Si intentas insertar un descuento para un código que no está en productos, la base de datos rechaza la operación lanzando sqlite3.IntegrityError (lo veremos en detalle en la parte 2).
PRAGMA foreign_keys — activar la comprobación¶
SQLite tiene las claves foráneas desactivadas por defecto. Debemos activarlas explícitamente al inicio de cada conexión, justo después de crear el cursor:
Sin
PRAGMA foreign_keys = ON, SQLite acepta sin error inserciones que violan las claves foráneas. La integridad referencial queda sin protección y puedes crear filas huérfanas sin darte cuenta.
Ejemplo: orden de inserción con claves foráneas¶
El orden de los INSERT importa: primero debes insertar en la tabla referenciada (productos) y solo después en la tabla que contiene la clave foránea (descuentos).
Mala práctica: insertar el hijo antes que el padre
7. Mapeo de objetos Python a tablas SQL¶
De clase a tabla¶
Cada clase Python puede mapearse directamente a una tabla SQL. Los atributos se convierten en columnas y cada instancia se convierte en una fila.
| Elemento Python | Elemento SQL |
|---|---|
Clase Item |
Tabla productos |
Atributo codigo |
Columna codigo TEXT PRIMARY KEY |
Atributo nombre |
Columna nombre TEXT NOT NULL |
Atributo precio |
Columna precio REAL NOT NULL |
Atributo cantidad |
Columna cantidad INTEGER NOT NULL |
Item("A1", "Agua", 1.00, 10) |
Fila ('A1', 'Agua', 1.0, 10) |
La estructura de la clase determina la estructura de la tabla. Una vez creada la tabla, cada Item que creas en Python tiene su reflejo exacto como fila en productos.
Clases con herencia a dos tablas¶
Supón que tienes dos clases: Item con cuatro atributos (codigo, nombre, precio, cantidad) e ItemConDescuento que hereda de Item y añade porcentaje_descuento. Para representar esta herencia en SQL usamos dos tablas relacionadas:
productos— almacena los atributos comunes a todos los items.descuentos— almacena solo elporcentajede los items con descuento, vinculada aproductosmediante una clave foránea.
Un ItemConDescuento necesita dos INSERT: uno en productos y otro en descuentos.
La tabla
descuentossolo tiene filas para los productos que tienen descuento. UnItemnormal no aparece endescuentos. Esto permite distinguir en la BD qué productos tienen descuento y cuáles no.
Ejemplo: reconstruir objetos Python desde la base de datos¶
Para recuperar los datos y reconstruir los objetos Item e ItemConDescuento, combina un SELECT sobre productos con una consulta adicional a descuentos para cada fila: