Saltar a contenido

Lab guiado: Del modelo de objetos a la base de datos


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, UPDATE y DELETE.

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

  1. Descomprime el ZIP del proyecto en tu carpeta de trabajo.
  2. Abre en VS Code la carpeta que contiene el directorio expendedora/ (no abras expendedora/ directamente). El terminal integrado debe abrirse en ese directorio padre. Todos los comandos del lab se ejecutan desde ahí.
  3. Ejecuta los tests para verificar que todo funciona:
python -m unittest

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 al codigo en la clase Item: 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 en nombre, precio y cantidad porque ningún producto puede carecer de esos datos.

  • FOREIGN KEY (clave foránea): columna que apunta a la PRIMARY KEY de otra tabla, estableciendo una relación entre ambas. En nuestro caso, descuentos.codigo referencia a productos.codigo. Esto significa que solo puede existir una fila en descuentos si el código ya existe en productos — 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 en productos
  • Item("A2", "Papas", 1.50, 8)1 fila en productos
  • ItemConDescuento("D1", "Refresco", 2.50, 5, 20)1 fila en productos + 1 fila en descuentos

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_descuento que 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:

import sqlite3

# 1. Conectar a la base de datos (se crea el fichero si no existe)
conn = sqlite3.connect("expendedora.db")
cursor = conn.cursor()

# 2. Activar soporte de claves foráneas (SQLite lo trae desactivado por defecto)
cursor.execute("PRAGMA foreign_keys = ON")

# 3. Crear la tabla de productos
cursor.execute("""
    CREATE TABLE IF NOT EXISTS productos (
        codigo TEXT PRIMARY KEY,
        nombre TEXT NOT NULL,
        precio REAL NOT NULL,
        cantidad INTEGER NOT NULL
    )
""")

# 4. Crear la tabla de descuentos con clave foránea
cursor.execute("""
    CREATE TABLE IF NOT EXISTS descuentos (
        codigo TEXT PRIMARY KEY,
        porcentaje REAL NOT NULL,
        FOREIGN KEY (codigo) REFERENCES productos(codigo)
    )
""")

conn.commit()
print("Tablas creadas correctamente.")
conn.close()

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 = ON activa la comprobación de claves foráneas. Sin esto, SQLite las ignora silenciosamente.
  • Líneas 11-18: crea la tabla productos. PRIMARY KEY en codigo garantiza que no haya dos productos con el mismo código.
  • Líneas 21-27: crea la tabla descuentos. La línea FOREIGN KEY (codigo) REFERENCES productos(codigo) establece la relación: el codigo de descuentos debe existir en productos.
  • 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:

python crear_bd.py

Comprobación

Deberías ver:

Tablas creadas correctamente.

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:

import sqlite3

conn = sqlite3.connect("expendedora.db")
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON")

# Crear tablas
cursor.execute("""
    CREATE TABLE IF NOT EXISTS productos (
        codigo TEXT PRIMARY KEY,
        nombre TEXT NOT NULL,
        precio REAL NOT NULL,
        cantidad INTEGER NOT NULL
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS descuentos (
        codigo TEXT PRIMARY KEY,
        porcentaje REAL NOT NULL,
        FOREIGN KEY (codigo) REFERENCES productos(codigo)
    )
""")

# Insertar productos (los mismos que usa la app en datos_iniciales.py)
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)")

# Insertar descuento para el Refresco (D1 tiene 20% de descuento)
cursor.execute("INSERT INTO descuentos VALUES ('D1', 20)")

conn.commit()
print("Base de datos creada con datos iniciales.")
conn.close()

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:

python crear_bd.py

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

Base de datos creada con datos iniciales.

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

print("Base de datos creada con datos iniciales.")

# Consultar productos
print("\n--- Productos ---")
cursor.execute("SELECT * FROM productos")
productos = cursor.fetchall()
for fila in productos:
    print(fila)

# Consultar descuentos
print("\n--- Descuentos ---")
cursor.execute("SELECT * FROM descuentos")
descuentos = cursor.fetchall()
for fila in descuentos:
    print(fila)

conn.close()
  • 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:

rm expendedora.db
python crear_bd.py

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 Item e ItemConDescuento y los hemos mapeado a columnas SQL.
  • Hemos diseñado 2 tablas relacionadas mediante una clave foránea.
  • Hemos usado CREATE TABLE, INSERT INTO y SELECT para crear, poblar y consultar la base de datos.
  • Hemos aprendido que SQLite guarda todo en un fichero .db y que Python lo maneja con el módulo sqlite3.
  • 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:

"""Script para crear la base de datos de la expendedora con datos iniciales."""

import sqlite3
import os

# Eliminar la base de datos si ya existe (para recrearla limpia)
if os.path.exists("expendedora.db"):
    os.remove("expendedora.db")

conn = sqlite3.connect("expendedora.db")
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON")

# Crear tablas
cursor.execute("""
    CREATE TABLE IF NOT EXISTS productos (
        codigo TEXT PRIMARY KEY,
        nombre TEXT NOT NULL,
        precio REAL NOT NULL,
        cantidad INTEGER NOT NULL
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS descuentos (
        codigo TEXT PRIMARY KEY,
        porcentaje REAL NOT NULL,
        FOREIGN KEY (codigo) REFERENCES productos(codigo)
    )
""")

# Insertar productos
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)")

# Insertar descuento
cursor.execute("INSERT INTO descuentos VALUES ('D1', 20)")

conn.commit()
print("Base de datos creada con datos iniciales.")

# Verificar: mostrar contenido de ambas tablas
print("\n--- Productos ---")
cursor.execute("SELECT * FROM productos")
for fila in cursor.fetchall():
    print(fila)

print("\n--- Descuentos ---")
cursor.execute("SELECT * FROM descuentos")
for fila in cursor.fetchall():
    print(fila)

conn.close()
print("\nBase de datos guardada en expendedora.db")

Ejecuta:

python crear_bd.py

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:

ls -la expendedora.db

Prueba manual guiada

  1. Ejecuta python crear_bd.py — debe mostrar la salida esperada.
  2. Comprueba que el fichero expendedora.db se ha creado.
  3. Ejecuta el script otra vez — debe funcionar igual (el script elimina y recrea la BD).
  4. Verifica que los tests del proyecto siguen pasando (no hemos tocado el código de la app; ejecuta desde la carpeta padre de expendedora/):
python -m unittest

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 Item e ItemConDescuento y su correspondencia con columnas SQL.
  • He comprendido el diseño de 2 tablas relacionadas con clave foránea.
  • He creado el script crear_bd.py que 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:

sudo apt install sqlite3

Windows (Git Bash):

  1. Ve a https://www.sqlite.org/download.html
  2. En la sección Precompiled Binaries for Windows, descarga el ZIP de sqlite-tools (por ejemplo sqlite-tools-win-x64-*.zip).
  3. Extrae el ZIP. Obtendrás un fichero sqlite3.exe.
  4. Copia sqlite3.exe a una carpeta que esté en el PATH, por ejemplo C:\Program Files\Git\usr\bin\ (así Git Bash lo encuentra directamente).
  5. Abre Git Bash y verifica con: sqlite3 --version

Una vez instalado (Linux o Windows), explora la base de datos:

sqlite3 expendedora.db

Dentro del prompt de SQLite, prueba:

.tables
.schema productos
SELECT * FROM productos;
.quit

¿Qué comando muestra la estructura de una tabla? ¿Qué diferencia hay entre .schema y SELECT?

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.