Saltar a contenido

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 enterotipo
  • no pueda ser negativarestricción
  • que un pedido no pueda existir si no está asociado a un cliente realintegridad (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:

1
2
3
datos = {}                         ## clave = codigo, valor = objeto Item
datos["A1"] = Item("A1", "Agua", 1.00, 10)
datos["A2"] = Item("A2", "Papas", 1.50, 8)

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 .db en 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:

1
2
3
4
5
6
7
8
9
import sqlite3

conn   = sqlite3.connect("expendedora.db")   ## 1. Conectar (crea el fichero si no existe)
cursor = conn.cursor()                       ## 2. Crear el cursor

cursor.execute("SELECT * FROM productos")    ## 3. Ejecutar SQL

conn.commit()                                ## 4. Confirmar cambios en disco
conn.close()                                 ## 5. Cerrar la conexión

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.

conn   = sqlite3.connect("expendedora.db")   ## abre o crea el fichero .db
cursor = conn.cursor()                       ## objeto para ejecutar SQL

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.

1
2
3
4
cursor.execute("INSERT INTO productos VALUES ('A1', 'Agua', 1.00, 10)")
cursor.execute("INSERT INTO productos VALUES ('A2', 'Papas', 1.50, 8)")
conn.commit()   ## ambos INSERT se guardan juntos en disco
conn.close()

Mala práctica: olvidar el commit

## Incorrecto: los cambios nunca llegan al fichero .db
cursor.execute("INSERT INTO productos VALUES ('A1', 'Agua', 1.00, 10)")
conn.close()
## Correcto
cursor.execute("INSERT INTO productos VALUES ('A1', 'Agua', 1.00, 10)")
conn.commit()
conn.close()


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():

1
2
3
4
5
6
7
8
cursor.execute("""
    CREATE TABLE IF NOT EXISTS productos (
        codigo   TEXT    PRIMARY KEY,
        nombre   TEXT    NOT NULL,
        precio   REAL    NOT NULL,
        cantidad INTEGER NOT NULL
    )
""")

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.

1
2
3
4
5
6
7
8
cursor.execute("""
    CREATE TABLE IF NOT EXISTS productos (
        codigo   TEXT    PRIMARY KEY,  ## identificador único, nunca nulo
        nombre   TEXT    NOT NULL,     ## obligatorio
        precio   REAL    NOT NULL,     ## obligatorio
        cantidad INTEGER NOT NULL      ## obligatorio
    )
""")

Si intentas insertar dos productos con el mismo codigo, SQLite lanza una excepción sqlite3.IntegrityError porque viola la restricción PRIMARY KEY (lo veremos en detalle en la parte 2).

Si no usamos la cláusula IF NOT EXISTS al crear la tabla y esta ya existe, SQLite lanza una excepción sqlite3.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.

INSERT INTO nombre_tabla VALUES (valor1, valor2, ...)

En Python, cada INSERT es una llamada a cursor.execute():

1
2
3
4
cursor.execute("INSERT INTO productos VALUES ('A1', 'Agua', 1.00, 10)")
cursor.execute("INSERT INTO productos VALUES ('A2', 'Papas', 1.50, 8)")
cursor.execute("INSERT INTO productos VALUES ('D1', 'Refresco', 2.50, 5)")
conn.commit()

Cuando los valores vienen de variables Python, usa parámetros ? en lugar de construir la cadena con f-strings o concatenación:

1
2
3
4
5
codigo   = "B1"
nombre   = "Chocolate"
precio   = 2.00
cantidad = 12
cursor.execute("INSERT INTO productos VALUES (?, ?, ?, ?)", (codigo, nombre, precio, cantidad))

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

## Incorrecto: un valor malicioso en `nombre` puede romper o manipular la sentencia SQL
cursor.execute(f"INSERT INTO productos VALUES ('{codigo}', '{nombre}', {precio}, {cantidad})")
## Correcto
cursor.execute("INSERT INTO productos VALUES (?, ?, ?, ?)", (codigo, nombre, precio, cantidad))

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:

INSERT INTO productos VALUES ('A1', 'Agua'); DROP TABLE productos; --', 1.2, 10)

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:

cursor.execute(
    "INSERT INTO productos VALUES (?, ?, ?, ?)",
    (codigo, nombre, precio, cantidad)
)

Aunque nombre valga:

"Agua'); DROP TABLE productos; --"

SQLite lo guarda como texto dentro del campo, no lo ejecuta. La consulta que se ejecuta sería:

INSERT INTO productos VALUES (
  'A1',
  'Agua''); DROP TABLE productos; --',
  1.20,
  10
)

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():

1
2
3
4
cursor.execute("SELECT * FROM productos")
filas = cursor.fetchall()              ## lista de tuplas
for fila in filas:
    print(fila)
('A1', 'Agua', 1.0, 10)
('A2', 'Papas', 1.5, 8)
('D1', 'Refresco', 2.5, 5)

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.

1
2
3
4
5
6
cursor.execute("SELECT * FROM productos WHERE codigo = ?", ("A1",))
fila = cursor.fetchone()   ## devuelve la primera fila como tupla, o None si no hay resultado
print(fila[0])             ## 'A1'   — codigo
print(fila[1])             ## 'Agua' — nombre
print(fila[2])             ## 1.0    — precio
print(fila[3])             ## 10     — cantidad

fetchone() avanza el cursor una posición. Si llamas a fetchone() varias veces seguidas sobre el mismo execute(), 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.

UPDATE tabla SET columna = nuevo_valor WHERE columna_pk = valor

En Python con parámetros ?:

1
2
3
4
5
cursor.execute(
    "UPDATE productos SET cantidad = ? WHERE codigo = ?",
    (9, "A1")
)
conn.commit()

Puedes actualizar varios campos a la vez separándolos con coma en el SET:

1
2
3
4
5
cursor.execute(
    "UPDATE productos SET precio = ?, cantidad = ? WHERE codigo = ?",
    (1.20, 15, "A1")
)
conn.commit()

Mala práctica: UPDATE sin WHERE

## Incorrecto: actualiza la cantidad en TODOS los productos
cursor.execute("UPDATE productos SET cantidad = 0")
## Correcto: solo el producto con código A1
cursor.execute("UPDATE productos SET cantidad = ? WHERE codigo = ?", (0, "A1"))

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.

DELETE FROM tabla WHERE columna = valor
cursor.execute("DELETE FROM productos WHERE codigo = ?", ("A1",))
conn.commit()

Con claves foráneas activas, hay que eliminar primero la tabla hija antes que la tabla padre; de lo contrario SQLite lanza IntegrityError:

1
2
3
4
cursor.execute("PRAGMA foreign_keys = ON")
cursor.execute("DELETE FROM descuentos WHERE codigo = ?", ("D1",))  ## hijo primero
cursor.execute("DELETE FROM productos WHERE codigo = ?", ("D1",))   ## padre después
conn.commit()

Mala práctica: DELETE sin WHERE

## Incorrecto: elimina TODOS los productos de la tabla
cursor.execute("DELETE FROM productos")
## Correcto: solo el producto indicado
cursor.execute("DELETE FROM productos WHERE codigo = ?", ("D1",))


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.

1
2
3
4
5
6
7
8
cursor.execute("""
    CREATE TABLE IF NOT EXISTS productos (
        codigo   TEXT    PRIMARY KEY,  ## identifica cada producto de forma única
        nombre   TEXT    NOT NULL,
        precio   REAL    NOT NULL,
        cantidad INTEGER NOT NULL
    )
""")

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 guarda NULL en la base de datos.

Ejemplo:

cursor.execute(
    "INSERT INTO productos (codigo, nombre) VALUES (?, ?)",
    ("A1", "")
)

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:

cursor.execute(
    "INSERT INTO productos (codigo, nombre) VALUES (?, ?)",
    ("A1", None)
)

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.

1
2
3
4
5
6
7
cursor.execute("""
    CREATE TABLE IF NOT EXISTS descuentos (
        codigo     TEXT PRIMARY KEY,
        porcentaje REAL NOT NULL,
        FOREIGN KEY (codigo) REFERENCES productos(codigo)
    )
""")

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:

1
2
3
conn   = sqlite3.connect("expendedora.db")
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON")  ## activa la comprobación de FK

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).

1
2
3
4
5
6
7
8
9
cursor.execute("PRAGMA foreign_keys = ON")

## Primero el producto base — tabla referenciada
cursor.execute("INSERT INTO productos VALUES ('D1', 'Refresco', 2.50, 5)")

## Después el descuento — tabla con FK, D1 ya existe en productos
cursor.execute("INSERT INTO descuentos VALUES ('D1', 20)")

conn.commit()

Mala práctica: insertar el hijo antes que el padre

## Incorrecto: D1 no existe todavía en productos → IntegrityError
cursor.execute("INSERT INTO descuentos VALUES ('D1', 20)")
cursor.execute("INSERT INTO productos VALUES ('D1', 'Refresco', 2.50, 5)")
## Correcto: primero el padre (productos), luego el hijo (descuentos)
cursor.execute("INSERT INTO productos VALUES ('D1', 'Refresco', 2.50, 5)")
cursor.execute("INSERT INTO descuentos VALUES ('D1', 20)")


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 el porcentaje de los items con descuento, vinculada a productos mediante una clave foránea.

Un ItemConDescuento necesita dos INSERT: uno en productos y otro en descuentos.

1
2
3
4
5
6
7
8
9
## Item normal → 1 INSERT en productos
cursor.execute("INSERT INTO productos VALUES ('A1', 'Agua', 1.00, 10)")
cursor.execute("INSERT INTO productos VALUES ('A2', 'Papas', 1.50, 8)")

## ItemConDescuento → 1 INSERT en productos + 1 INSERT en descuentos
cursor.execute("INSERT INTO productos VALUES ('D1', 'Refresco', 2.50, 5)")
cursor.execute("INSERT INTO descuentos VALUES ('D1', 20)")

conn.commit()

La tabla descuentos solo tiene filas para los productos que tienen descuento. Un Item normal no aparece en descuentos. 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:

from item import Item, ItemConDescuento  ## importa las clases del módulo donde las tengas definidas

cursor.execute("SELECT * FROM productos")
for fila in cursor.fetchall():
    codigo, nombre, precio, cantidad = fila       ## desempaquetar la tupla

    cursor.execute("SELECT porcentaje FROM descuentos WHERE codigo = ?", (codigo,))  ## reutilizamos el mismo cursor — fetchall() ya cargó todos los productos en memoria
    descuento = cursor.fetchone()

    if descuento:                                 ## tiene entrada en descuentos
        item = ItemConDescuento(codigo, nombre, precio, cantidad, descuento[0])
    else:                                         ## item normal
        item = Item(codigo, nombre, precio, cantidad)
    print(item)