Lab guiado: Del modelo de objetos a la base de datos¶
- Unidad: UT3 — Persistencia con bases de datos
- Agrupamiento: Parejas (conductor/navegante)
- Recursos:
- Carpeta comprimida del proyecto expendedora: expendedora.zip
- Ficheros de trabajo:
crear_bd.py,expendedora.db
Cómo trabajar en parejas¶
Este lab se realiza en parejas conductor/navegante. Leed las instrucciones antes de empezar.
Roles¶
| Rol | Qué hace |
|---|---|
| Conductor | Escribe el código/configuración y ejecuta los pasos. Solo el conductor toca el teclado. |
| Navegante | Lee el enunciado en voz alta, propone soluciones, detecta errores y formula las preguntas. |
Rotación¶
- Cambiad de rol al final de cada paso.
- Al rotar, el navegante ocupa el teclado y el conductor se convierte en navegante.
- Ambos debéis poder explicar cualquier parte del trabajo al terminar el lab.
Entrega¶
Carpeta comprimida en formato zip que contenga el fichero crear_bd.py y el fichero expendedora.db generado.
Antes de empezar: repasemos los conceptos
- Base de datos relacional: almacena información en tablas (filas y columnas). Cada tabla guarda un tipo de dato concreto.
- Tabla: estructura con columnas fijas (nombre, tipo) y filas que representan registros individuales. Similar a una hoja de cálculo.
- PRIMARY KEY: columna que identifica de forma única cada fila. No puede repetirse ni estar vacía.
- FOREIGN KEY (clave foránea): columna que apunta a la PRIMARY KEY de otra tabla, creando una relación entre ambas.
- SQLite: motor de base de datos que guarda todo en un solo fichero
.db. Viene incluido en Python, no necesita instalación. - SQL: lenguaje para crear tablas, insertar datos y hacer consultas. Las sentencias principales son
CREATE TABLE,INSERT,SELECT,UPDATEyDELETE.
Paso 1 — Preparar el proyecto expendedora/¶
Conceptos
La aplicación Máquina Expendedora usa arquitectura por capas con un patrón repositorio para la persistencia. Actualmente guarda los datos en memoria (un diccionario). Nuestro objetivo en estos labs es sustituir esa persistencia por una base de datos SQLite.
Objetivo¶
Descomprimir el proyecto - descarga aquí - y verificar que funciona correctamente antes de empezar a modificarlo.
Tarea¶
- Descomprime el ZIP del proyecto en tu carpeta de trabajo.
- Abre en VS Code la carpeta que contiene el directorio
expendedora/(no abrasexpendedora/directamente). El terminal integrado debe abrirse en ese directorio padre. Todos los comandos del lab se ejecutan desde ahí. - Ejecuta los tests para verificar que todo funciona:
Comprobación¶
Deberías ver algo similar a:
..................................................
----------------------------------------------------------------------
Ran 48 tests in 0.Xs
OK
Todos los tests deben pasar. Si alguno falla, revisa la instalación antes de continuar.
Paso 2 — Analizar las entidades del dominio domain/item.py¶
Conceptos
Para diseñar una base de datos, primero necesitamos entender qué datos maneja la aplicación. En nuestro caso, las entidades están en domain/item.py.
Objetivo¶
Identificar los atributos de cada entidad y sus tipos de datos, como paso previo al diseño de las tablas.
Tarea¶
Abre el fichero domain/item.py y revisa las dos clases:
Clase Item — producto base:
| Atributo | Tipo Python | Descripción |
|---|---|---|
codigo |
str |
Identificador único (ej: "A1") |
nombre |
str |
Nombre del producto |
precio |
float |
Precio en euros |
cantidad |
int |
Unidades en stock |
Clase ItemConDescuento — hereda de Item y añade:
| Atributo | Tipo Python | Descripción |
|---|---|---|
porcentaje_descuento |
float |
Descuento entre 0 y 100 |
Ahora revisa infrastructure/datos_iniciales.py para ver los 3 productos que usa la aplicación:
repo.guardar(Item("A1", "Agua", 1.00, 10))
repo.guardar(Item("A2", "Papas", 1.50, 8))
repo.guardar(ItemConDescuento("D1", "Refresco", 2.50, 5, 20))
Pregunta:¶
¿Qué diferencia hay entre los datos de "Agua" y los de "Refresco"?
Respuesta
"Agua" es un Item normal con 4 atributos. "Refresco" es un ItemConDescuento que tiene los mismos 4 atributos más un porcentaje_descuento de 20. Esta diferencia determinará el diseño de nuestras tablas.
Paso 3 — Diseñar las tablas de la base de datos¶
Conceptos
Cada clase Python se puede mapear a una tabla SQL. Los atributos se convierten en columnas, y cada objeto se convierte en una fila. Cuando hay herencia (como ItemConDescuento hereda de Item), una opción es usar dos tablas relacionadas mediante una clave foránea.
Objetivo¶
Diseñar las dos tablas que almacenarán los productos de la expendedora.
Tarea¶
Usaremos 2 tablas:
Antes de ver las tablas, aclaremos tres conceptos clave:
-
PRIMARY KEY(clave primaria): columna que identifica de forma única e irrepetible cada fila de la tabla. No puede haber dos filas con el mismo valor, y no puede estar vacía. Es el equivalente alcodigoen la claseItem: regla de negocio que dice que no pueden existir dos productos con el mismo código. -
NOT NULL: restricción que obliga a que esa columna siempre tenga un valor. Si intentas insertar una fila sin ese campo, la base de datos rechaza la operación. Lo usamos ennombre,precioycantidadporque ningún producto puede carecer de esos datos. -
FOREIGN KEY(clave foránea): columna que apunta a laPRIMARY KEYde otra tabla, estableciendo una relación entre ambas. En nuestro caso,descuentos.codigoreferencia aproductos.codigo. Esto significa que solo puede existir una fila endescuentossi el código ya existe enproductos— la base de datos impide dejar referencias "huérfanas".
Tabla productos — almacena los datos comunes de todo producto:
| Columna | Tipo SQL | Restricción | Equivale a |
|---|---|---|---|
codigo |
TEXT |
PRIMARY KEY |
Item.codigo |
nombre |
TEXT |
NOT NULL |
Item.nombre |
precio |
REAL |
NOT NULL |
Item.precio |
cantidad |
INTEGER |
NOT NULL |
Item.cantidad |
Tabla descuentos — solo tiene filas para productos con descuento:
| Columna | Tipo SQL | Restricción | Equivale a |
|---|---|---|---|
codigo |
TEXT |
PRIMARY KEY, FOREIGN KEY → productos |
ItemConDescuento.codigo |
porcentaje |
REAL |
NOT NULL |
ItemConDescuento.porcentaje_descuento |
Correspondencia con los tipos Python:
| Python | SQL | Ejemplo |
|---|---|---|
str |
TEXT |
"A1", "Agua" |
float |
REAL |
1.00, 20.0 |
int |
INTEGER |
10, 5 |
Así queda el mapeo de objetos a filas:
Item("A1", "Agua", 1.00, 10)→ 1 fila enproductosItem("A2", "Papas", 1.50, 8)→ 1 fila enproductosItemConDescuento("D1", "Refresco", 2.50, 5, 20)→ 1 fila enproductos+ 1 fila endescuentos
La FOREIGN KEY en descuentos.codigo garantiza que no pueda existir un descuento para un producto que no exista en productos. Si intentas insertar en descuentos un código que no está en productos, la base de datos lo rechaza automáticamente.
Pregunta:¶
¿Por qué no usamos una sola tabla con una columna
porcentaje_descuentoque sea NULL para productos sin descuento?
Respuesta
Ambos diseños serían válidos. Usamos dos tablas para aprender el concepto de clave foránea y relaciones entre tablas, que es fundamental en bases de datos relacionales. En un proyecto real, la elección depende del caso de uso.
Paso 4 — Escribir las sentencias CREATE TABLE crear_bd.py¶
Conceptos
CREATE TABLE es la sentencia SQL que define la estructura de una tabla. Se indican las columnas, sus tipos, y las restricciones. IF NOT EXISTS evita errores si la tabla ya fue creada.
Objetivo¶
Escribir las sentencias SQL que crean las dos tablas.
Tarea¶
Crea un fichero crear_bd.py en la raíz del proyecto (al mismo nivel que la carpeta expendedora/). Empieza con este código:
Repasemos cada parte:
- Línea 4:
sqlite3.connect("expendedora.db")crea el fichero de base de datos si no existe, o lo abre si ya existe. - Línea 8:
PRAGMA foreign_keys = ONactiva la comprobación de claves foráneas. Sin esto, SQLite las ignora silenciosamente. - Líneas 11-18: crea la tabla
productos.PRIMARY KEYencodigogarantiza que no haya dos productos con el mismo código. - Líneas 21-27: crea la tabla
descuentos. La líneaFOREIGN KEY (codigo) REFERENCES productos(codigo)establece la relación: elcodigodedescuentosdebe existir enproductos. - Línea 29:
commit()guarda los cambios en el fichero. Sin commit, los cambios se pierden.
Python, SQL y valores nulos
NOT NULL en SQL significa que esa columna siempre debe tener un valor. Es importante saber cómo se traduce desde Python:
| Python | SQL | ¿Cumple NOT NULL? |
|---|---|---|
"" |
texto vacío '' |
✅ Sí — no es NULL, es texto vacío |
None |
NULL |
❌ No — SQLite lo rechaza con error |
0 / False |
0 |
✅ Sí — tiene valor |
Regla práctica: antes de insertar datos que vienen de variables Python, verifica que ningún campo obligatorio sea None. En labs posteriores veremos cómo gestionar estos errores automáticamente.
Ejecuta el script:
Comprobación¶
Deberías ver:
Y se habrá creado el fichero expendedora.db en la raíz del proyecto.
Paso 5 — Insertar los datos iniciales crear_bd.py¶
Conceptos
INSERT INTO es la sentencia SQL para añadir filas a una tabla. La sintaxis es INSERT INTO tabla VALUES (valor1, valor2, ...). Los valores de texto van entre comillas.
Objetivo¶
Insertar los 3 productos iniciales de la expendedora en la base de datos.
Tarea¶
Borra el fichero expendedora.db que se generó en el paso anterior (lo vamos a recrear con datos). Sustituye el contenido de crear_bd.py por este código completo:
Fíjate en la correspondencia con datos_iniciales.py:
| Python | SQL |
|---|---|
Item("A1", "Agua", 1.00, 10) |
INSERT INTO productos VALUES ('A1', 'Agua', 1.00, 10) |
Item("A2", "Papas", 1.50, 8) |
INSERT INTO productos VALUES ('A2', 'Papas', 1.50, 8) |
ItemConDescuento("D1", "Refresco", 2.50, 5, 20) |
INSERT INTO productos VALUES ('D1', 'Refresco', 2.50, 5) + INSERT INTO descuentos VALUES ('D1', 20) |
El ItemConDescuento necesita dos INSERT: uno en productos (datos base) y otro en descuentos (el porcentaje). El orden importa: primero productos, porque la clave foránea de descuentos exige que el código ya exista en productos.
Ejecuta el script:
Pregunta:¶
¿Qué pasaría si intentaras insertar el descuento ANTES que el producto (invertir el orden de los INSERT)?
Respuesta
Daría un error de clave foránea, porque descuentos.codigo referencia a productos.codigo. No puedes crear un descuento para un producto que aún no existe en la tabla productos. Por eso activamos PRAGMA foreign_keys = ON.
Comprobación¶
Paso 6 — Consultar los datos con SELECT crear_bd.py¶
Conceptos
SELECT es la sentencia SQL para leer datos. SELECT * FROM tabla devuelve todas las filas y columnas. cursor.fetchall() recoge los resultados en una lista de tuplas Python.
Objetivo¶
Verificar que los datos se insertaron correctamente consultando ambas tablas.
Tarea¶
Añade las siguientes líneas antes de conn.close() en crear_bd.py (sustituye la línea conn.close() y lo que hay después del print):
cursor.fetchall()devuelve una lista de tuplas. Cada tupla es una fila de la tabla.- Al hacer
print(fila), Python muestra la tupla con sus valores.
Borra expendedora.db y ejecuta de nuevo:
Comprobación¶
Base de datos creada con datos iniciales.
--- Productos ---
('A1', 'Agua', 1.0, 10)
('A2', 'Papas', 1.5, 8)
('D1', 'Refresco', 2.5, 5)
--- Descuentos ---
('D1', 20.0)
Los 3 productos están en la tabla productos, y solo D1 (Refresco) tiene entrada en descuentos con su 20% de descuento.
Reflexión¶
Respuestas
- Hemos identificado los atributos de
ItemeItemConDescuentoy los hemos mapeado a columnas SQL. - Hemos diseñado 2 tablas relacionadas mediante una clave foránea.
- Hemos usado
CREATE TABLE,INSERT INTOySELECTpara crear, poblar y consultar la base de datos. - Hemos aprendido que SQLite guarda todo en un fichero
.dby que Python lo maneja con el módulosqlite3. - La clave foránea garantiza la integridad: no puede haber descuentos huérfanos.
Paso 7 — Script completo y verificación final crear_bd.py¶
Conceptos
Es buena práctica tener un script limpio y completo que pueda recrear la base de datos desde cero. Esto facilita las pruebas y el desarrollo.
Objetivo¶
Tener el script final completo y verificar que la base de datos se genera correctamente.
Tarea¶
Aquí tienes el script crear_bd.py completo. Asegúrate de que tu fichero coincide:
Ejecuta:
Comprobación¶
Base de datos creada con datos iniciales.
--- Productos ---
('A1', 'Agua', 1.0, 10)
('A2', 'Papas', 1.5, 8)
('D1', 'Refresco', 2.5, 5)
--- Descuentos ---
('D1', 20.0)
Base de datos guardada en expendedora.db
Verifica también que el fichero expendedora.db existe en la raíz del proyecto:
Prueba manual guiada¶
- Ejecuta
python crear_bd.py— debe mostrar la salida esperada. - Comprueba que el fichero
expendedora.dbse ha creado. - Ejecuta el script otra vez — debe funcionar igual (el script elimina y recrea la BD).
- Verifica que los tests del proyecto siguen pasando (no hemos tocado el código de la app; ejecuta desde la carpeta padre de
expendedora/):
Kata de refactor¶
Kata 1
Añade un cuarto producto sin descuento a la base de datos: código "B1", nombre "Chocolate", precio 2.00, cantidad 12. Añade el INSERT correspondiente al script y ejecuta para verificar.
Kata 2
Añade un quinto producto con descuento: código "D2", nombre "Zumo", precio 3.00, cantidad 6, descuento 15%. Recuerda que necesitas dos INSERT (uno en cada tabla).
Kata 3 — avanzado
Modifica el script para que en lugar de hacer print(fila) (que muestra tuplas), muestre los productos en un formato legible: A1 - Agua - 1.00 EUR - stock 10. Pista: cada fila es una tupla y puedes acceder a sus elementos con fila[0], fila[1], etc.
Checklist de entrega¶
- He descomprimido el proyecto y verificado que los tests pasan.
- He identificado los atributos de
ItemeItemConDescuentoy su correspondencia con columnas SQL. - He comprendido el diseño de 2 tablas relacionadas con clave foránea.
- He creado el script
crear_bd.pyque genera la base de datos con datos iniciales. - He verificado que la base de datos contiene los 3 productos y 1 descuento correctos.
Actividades de ampliación¶
A. Explorar SQLite desde la terminal (requiere Paso 7)¶
Instala la herramienta de línea de comandos de SQLite según tu sistema operativo:
Linux:
Windows (Git Bash):
- Ve a https://www.sqlite.org/download.html
- En la sección Precompiled Binaries for Windows, descarga el ZIP de sqlite-tools (por ejemplo
sqlite-tools-win-x64-*.zip). - Extrae el ZIP. Obtendrás un fichero
sqlite3.exe. - Copia
sqlite3.exea una carpeta que esté en el PATH, por ejemploC:\Program Files\Git\usr\bin\(así Git Bash lo encuentra directamente). - Abre Git Bash y verifica con:
sqlite3 --version
Una vez instalado (Linux o Windows), explora la base de datos:
Dentro del prompt de SQLite, prueba:
¿Qué comando muestra la estructura de una tabla? ¿Qué diferencia hay entre
.schemaySELECT?
B. Probar la integridad de la clave foránea (requiere Paso 7)¶
Crea un fichero ampliacion_b.py que intente insertar un descuento para un producto que no existe:
import sqlite3
conn = sqlite3.connect("expendedora.db")
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON")
# Intentar insertar un descuento para un código que no existe en productos
try:
cursor.execute("INSERT INTO descuentos VALUES ('X9', 50)")
conn.commit()
print("Insertado correctamente")
except sqlite3.IntegrityError as e:
print(f"Error de integridad: {e}")
conn.close()
¿Qué error obtienes? ¿Qué pasaría si no hubieras activado
PRAGMA foreign_keys = ON?
C. Insertar datos desde variables Python (requiere Paso 7)¶
Crea un fichero ampliacion_c.py que inserte un producto usando variables en lugar de valores directos. Usa parámetros ? para evitar inyección SQL:
import sqlite3
conn = sqlite3.connect("expendedora.db")
cursor = conn.cursor()
codigo = "C1"
nombre = "Galletas"
precio = 1.75
cantidad = 15
cursor.execute(
"INSERT INTO productos VALUES (?, ?, ?, ?)",
(codigo, nombre, precio, cantidad)
)
conn.commit()
# Verificar
cursor.execute("SELECT * FROM productos WHERE codigo = ?", (codigo,))
print(cursor.fetchone())
conn.close()
¿Por qué usamos
?en lugar de escribir los valores directamente en la cadena SQL?
D. Reto final¶
Sin plantilla ni pistas: crea un script ampliacion_d.py que lea los datos del fichero expendedora.db, cree objetos Item e ItemConDescuento a partir de las filas, y muestre cada producto en formato legible. Necesitarás importar las clases desde expendedora.domain.item. Pista: mostrar_producto() devuelve una tupla (codigo, nombre, precio_base, precio_final, cantidad, porcentaje_descuento) — puedes desempaquetarla para formatear la salida.