Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 63: Bases de datos con SQLite y SQLAlchemy — la biblioteca virtual

100 Días de Código en Python — Día 63: Bases de datos con SQLite y SQLAlchemy — la biblioteca virtual

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 63 (nivel avanzado).

Este tutorial adapta pedagógicamente el Día 63. Las secciones 📝 Contenido original del curso y 📝 Ejercicio original del curso provienen de las lecciones 443 a 450; las soluciones oficiales están en los gists enlazados y los archivos de inicio y final (day-63-starting-files-library-project.zip, day-63-library-project-end.zip) en los recursos. Los retos 🟢🟡🟠🔴, las secciones sobre inyección SQL y por qué borrar con un enlace es mala idea, el proyecto extra y la etapa 5 del proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14, Flask 3.1.3, Flask-SQLAlchemy 3.1.1 y SQLAlchemy 2.1.1, se ejecutó todo el código de las lecciones 447 a 449. Se comprobaron los tres errores de SQL que menciona el curso, que el reto de la lección 448 no puede cumplirse con los datos que da (falta title y aparece review en lugar de rating), que la tabla se llama book y no books si no se indica, que Flask-SQLAlchemy crea sola la carpeta instance, que el resultado de scalars() solo se puede recorrer una vez, y el comportamiento de get_or_404.


Tabla de contenidos

  1. Antes de empezar: la biblioteca que olvidaba sus libros
  2. El sitio sin base de datos
  3. ¿Qué pasa al reiniciar el servidor?
  4. SQLite y SQL directo
  5. SQLAlchemy: la base de datos como clases de Python
  6. CRUD con SQLAlchemy
  7. 🚀 Proyecto del día: la biblioteca virtual con base de datos
  8. 🌟 Proyecto extra: el préstamo de balones del colegio
  9. 🏗️ Proyecto integrador del bloque avanzado — Etapa 5
  10. 🧩 Comprueba lo aprendido
  11. 🤖 Programar con apoyo de IA
  12. 🛠️ Correcciones técnicas al material original
  13. 📋 Matriz de trazabilidad
  14. 📦 Evidencias de aprendizaje del Día 63

0. Antes de empezar: la biblioteca que olvidaba sus libros

(Fuente: 443-day-63-goals-creating-a-virtual-bookshelf.md)

📝 Contenido original del curso

¿Has querido llevar un registro de los libros que has leído y calificarlos? Hay empresas dedicadas a eso (por ejemplo, LibraryThing). Para hacerlo necesitamos una base de datos. Al final del día sabrás crear una base de datos SQLite y crear, leer, actualizar y borrar datos en ella (CRUD), y conectarla con una aplicación Flask.

Prerequisitos: Flask con formularios y POST (Días 60 y 61), clases (Días 16 a 21), listas de diccionarios (Día 9), url_for y rutas con variables (Días 55 a 57).

📖 ¿Por qué no seguir con CSV? (adaptación)

En el Día 62 guardaste datos en un CSV. Funciona, pero:

Necesidad CSV Base de datos
Buscar un registro Leer todo el archivo Consulta directa (WHERE)
Cambiar un dato Reescribir el archivo entero UPDATE de ese registro
Evitar duplicados o campos vacíos Tu código debe revisarlo Reglas en la tabla (UNIQUE, NOT NULL)
Dos usuarios guardando a la vez Riesgo de perder datos La base de datos lo coordina
Relacionar datos (autores y libros) Difícil Para eso está (Día 69)

1. El sitio sin base de datos

(Fuentes: 444-download-the-starting-project.md y 445-make-the-website-work.md)

📝 Ejercicio original del curso

Descarga el proyecto de inicio (plantillas para Jinja), crea el entorno virtual e instala requirements.txt (como en los Días 61 y 62). Luego, cinco retos en main.py, index.html y add.html:

  1. En la ruta /, un <h1> "My Library" y un enlace "Add New Book".
  2. En /add, un formulario con título, autor y calificación, y un botón "Add Book".
  3. Al enviar el formulario, el libro se agrega como diccionario a la lista all_books:
Python
   all_books = [
       {
           "title": "Harry Potter",
           "author": "J. K. Rowling",
           "rating": 9,
       }
   ]
  1. La portada muestra cada libro como <li> dentro de un <ul>.
  2. Si no hay libros (all_books = []), la portada muestra <p>Library is empty.</p>; el enlace "Add New Book" lleva a /add.

🔍 Antes de programar

  • ¿Qué ruta necesita aceptar POST?
  • ¿Cómo sabe la plantilla si la lista está vacía? (Pista: {% if all_books %} o {% for %}…{% else %}, Día 57.)
  • Después de agregar un libro, ¿muestras la portada con render_template o rediriges con redirect(url_for("home"))? (Día 60: POST/Redirect/GET.)

2. ¿Qué pasa al reiniciar el servidor?

(Fuente: 446-what-happens-when-you-refresh-the-server.md)

📝 Contenido original del curso

Agrega algunos libros; aparecen en la portada. Ahora detén el servidor y vuelve a ejecutarlo. Recarga la portada: ¿qué pasó con tus libros?

Desaparecieron. Estaban en la lista all_books, una variable que se crea de nuevo cada vez que se ejecuta main.py. "Si esto le pasara a los datos de nuestros usuarios, no confiarían mucho en nuestro sitio". Hace falta persistencia de datos: guardarlos en una base de datos.

📖 Memoria contra almacenamiento (adaptación)

Variable en Python (RAM) Archivo o base de datos (disco)
Velocidad Muy rápida Más lenta
¿Sobrevive al reinicio? ❌ ✅
Ejemplo all_books = [] books.db

Con el modo de depuración, basta con guardar main.py para que el servidor se reinicie y la lista se vacíe: lo notarás muy rápido.


3. SQLite y SQL directo

(Fuente: 447-sqlite-databases.md)

🎯 Objetivo de aprendizaje

Crear una base de datos SQLite, una tabla con restricciones y un registro, usando SQL desde Python.

📝 Contenido original del curso

SQLite es la base de datos más usada del mundo y viene incluida en Python. En un proyecto aparte:

Python
import sqlite3

db = sqlite3.connect("books-collection.db")   # si no existe, la crea
cursor = db.cursor()
  • Al ejecutarlo aparece el archivo books-collection.db (no intentes abrirlo en PyCharm).
  • El cursor es como el puntero del ratón en una hoja de cálculo: lo que agrega, edita o borra datos.

Crear una tabla. Un archivo de Excel puede tener varias hojas; una base de datos puede tener varias tablas:

Python
cursor.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title varchar(250) NOT NULL UNIQUE, author varchar(250) NOT NULL, rating FLOAT NOT NULL)")
Parte Significado
execute() Ejecuta una instrucción en SQL (Structured Query Language), casi como frases en inglés con palabras clave en MAYÚSCULAS
CREATE TABLE books Crea la tabla books
id INTEGER PRIMARY KEY Entero, clave primaria: identifica de forma única cada registro (como el número de pasaporte de una persona)
title varchar(250) NOT NULL UNIQUE Texto de hasta 250 caracteres, obligatorio y sin repetirse
author varchar(250) NOT NULL Texto obligatorio
rating FLOAT NOT NULL Número decimal obligatorio

Ver la base de datos: instala DB Browser for SQLite (sqlitebrowser.org; en Windows, el instalador estándar), pulsa Open Database y abre el archivo .db: verás la tabla books con sus 4 campos.

Insertar un registro:

Python
cursor.execute("INSERT INTO books VALUES(1, 'Harry Potter', 'J. K. Rowling', '9.3')")
db.commit()

commit() guarda los cambios. Antes de volver a ejecutar, comenta la línea CREATE TABLE (si no: sqlite3.OperationalError: table books already exists) y cierra la base de datos en DB Browser (si no: aviso de database locked).

SQL es muy sensible a errores de escritura: con VALUE en lugar de VALUES, "no funciona en absoluto (¿puedes ver la diferencia?)". Por suerte hay una forma mejor: SQLAlchemy.

📖 Los errores del curso, verificados

Instrucción Error (Python 3.14)
Crear la tabla dos veces OperationalError: table books already exists
INSERT INTO books VALUE(...) OperationalError: near "VALUE": syntax error
Insertar otro libro con el título "Harry Potter" IntegrityError: UNIQUE constraint failed: books.title

Una curiosidad: el curso inserta la calificación como texto ('9.3'), pero SQLite la guarda como número (real) porque la columna es FLOAT (verificado). SQLite es flexible con los tipos; otras bases de datos no lo son. Mejor escribir 9.3 sin comillas.

CREATE TABLE IF NOT EXISTS books (...) evita tener que comentar la línea.

📖 🔒 Inyección SQL: nunca armes SQL con f-strings (adaptación)

Si el título lo escribe un usuario y armas la consulta así:

Python
cursor.execute(f"INSERT INTO books (title, author, rating) VALUES ('{titulo}', '{autor}', {nota})")

alguien podría escribir un "título" que cierre tu texto y agregue instrucciones SQL propias (por ejemplo, borrar la tabla). Es la inyección SQL, uno de los ataques más conocidos. La defensa: parámetros con ?, y sqlite3 se encarga del resto (verificado con "El túnel"):

Python
cursor.execute("INSERT INTO books (title, author, rating) VALUES (?, ?, ?)", (titulo, autor, nota))

SQLAlchemy (sección 4) usa parámetros automáticamente.

⚠️ Errores frecuentes

Error Causa Solución
Los datos no aparecen en DB Browser Faltó db.commit() Siempre commit() después de modificar
database is locked DB Browser tiene la base abierta con cambios sin guardar Ciérrala o guarda en DB Browser
La base se crea en otra carpeta La ruta es relativa a la carpeta desde donde ejecutas Ejecuta desde la carpeta del proyecto o usa una ruta absoluta

4. SQLAlchemy: la base de datos como clases de Python

(Fuente: 448-sqlalchemy.md)

🎯 Objetivo de aprendizaje

Definir una tabla como una clase (modelo) de SQLAlchemy y crearla desde Flask.

📝 Contenido original del curso

Escribir SQL es complicado y propenso a errores; sería mejor escribir Python y que el editor nos ayude a ver los errores. Para eso existe SQLAlchemy, una biblioteca ORM (Object Relational Mapping): las tablas se vuelven clases, los campos se vuelven atributos y cada fila es un objeto.

  1. Comenta el código de sqlite3.
  2. Instala flask, SQLAlchemy y flask_sqlalchemy desde requirements.txt.
  3. Importa:
Python
   from flask import Flask
   from flask_sqlalchemy import SQLAlchemy
   from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
   from sqlalchemy import Integer, String, Float
  1. Reto, con la documentación de Flask-SQLAlchemy 3.1:

    • una base de datos SQLite new-books-collection.db (inicializa la aplicación);
    • una tabla books con id, title, author y rating, con las mismas restricciones que antes;
    • con el "contexto de la aplicación", crear el esquema;
    • con el contexto, crear un registro con: id: 1, author: "J. K. Rowling", review: 9.3.

    Pistas: la URL de la base de datos es "sqlite:///new-books-collection.db"; puedes revisarla con DB Browser.

📖 La solución (lección 449, verificada)

Python
app = Flask(__name__)


class Base(DeclarativeBase):
    pass


app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///new-books-collection.db"
db = SQLAlchemy(model_class=Base)
db.init_app(app)


class Book(db.Model):
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    title: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
    author: Mapped[str] = mapped_column(String(250), nullable=False)
    rating: Mapped[float] = mapped_column(Float, nullable=False)


with app.app_context():
    db.create_all()
  • Desde Flask-SQLAlchemy 3.1 se pasa una subclase de DeclarativeBase al crear SQLAlchemy.
  • Los dos puntos (id: Mapped[int]) son anotaciones de tipo: le dicen a SQLAlchemy (y al editor) qué tipo de dato guarda cada campo.
  • app.app_context(): fuera de una solicitud web, Flask-SQLAlchemy necesita saber a qué aplicación pertenece la base de datos. Dentro de una ruta ya hay contexto; en el código suelto, hay que abrirlo con with.

🛠️ Nota técnica (verificado): el reto tiene tres detalles que no coinciden:

  1. Los datos del registro no incluyen title, que es NOT NULL: al guardarlo aparece IntegrityError: NOT NULL constraint failed: book.title. Agrega title="Harry Potter".
  2. Dice review: 9.3, pero el campo se llama rating: Book(review=9.3) lanza TypeError: 'review' is an invalid keyword argument for Book.
  3. Pide una tabla llamada books, pero SQLAlchemy la llama como la clase en minúsculas: book. Si necesitas el nombre exacto, agrega __tablename__ = "books" a la clase.

🛠️ Nota técnica (verificado): en Flask-SQLAlchemy 3.x, una ruta relativa como sqlite:///new-books-collection.db crea la base de datos en la carpeta instance del proyecto, no junto a main.py. La propia extensión crea esa carpeta al inicializarse. Si abres en DB Browser "otra" base vacía, probablemente estás mirando el archivo equivocado.


5. CRUD con SQLAlchemy

(Fuente: 449-crud-operations-with-sqlalchemy.md)

🎯 Objetivo de aprendizaje

Crear, leer, actualizar y borrar registros con SQLAlchemy 2.

📝 Contenido original del curso

"Lo más importante al aprender cualquier tecnología de bases de datos es saber hacer CRUD": Create, Read, Update, Delete.

Python
# CREATE
with app.app_context():
    new_book = Book(title="Harry Potter", author="J. K. Rowling", rating=9.3)   # el id se genera solo
    db.session.add(new_book)
    db.session.commit()

# READ: todos
with app.app_context():
    result = db.session.execute(db.select(Book).order_by(Book.title))
    all_books = result.scalars()

# READ: uno, por una condición
with app.app_context():
    book = db.session.execute(db.select(Book).where(Book.title == "Harry Potter")).scalar()

# UPDATE: por una condición
with app.app_context():
    book_to_update = db.session.execute(db.select(Book).where(Book.title == "Harry Potter")).scalar()
    book_to_update.title = "Harry Potter and the Chamber of Secrets"
    db.session.commit()

# UPDATE: por clave primaria
book_id = 1
with app.app_context():
    book_to_update = db.get_or_404(Book, book_id)
    book_to_update.title = "Harry Potter and the Goblet of Fire"
    db.session.commit()

# DELETE: por clave primaria
with app.app_context():
    book_to_delete = db.get_or_404(Book, book_id)
    db.session.delete(book_to_delete)
    db.session.commit()
  • Una consulta (db.select(...)) se ejecuta en una sesión y devuelve filas (un Result); scalars() da los objetos, scalar() da uno.
  • Desde Flask-SQLAlchemy 3.0, métodos antiguos como Book.query.get() están obsoletos. Verás mucho código viejo en Internet que los usa.

📖 Detalles verificados (adaptación)

Situación Resultado
.scalar() sin coincidencias None (si luego haces book.title, AttributeError)
db.get_or_404(Book, 99) con un id inexistente Lanza NotFound: en una ruta, Flask responde 404
Recorrer dos veces el resultado de scalars() La segunda vez está vacío
scalars().all() Una lista normal, que puedes recorrer, contar (len) y ordenar
Guardar un título repetido IntegrityError: UNIQUE constraint failed: book.title; después hay que hacer db.session.rollback()

La sesión es como un "carrito de compras": add, cambios en atributos y delete se acumulan, y commit() los paga todos juntos. Si algo falla, rollback() vacía el carrito.

⚠️ Errores frecuentes

Error Causa Solución
RuntimeError: Working outside of application context. Usaste db fuera de una ruta sin with app.app_context(): Abre el contexto
Los cambios no se guardan Faltó db.session.commit() commit()
La lista de libros sale vacía en la plantilla Recorriste el ScalarResult antes (por ejemplo, para contarlo) .scalars().all()
AttributeError: type object 'Book' has no attribute 'query' o avisos de obsoleto Código antiguo db.session.execute(db.select(...)) o db.get_or_404
sqlalchemy.exc.PendingRollbackError Un error anterior dejó la sesión a medias db.session.rollback() en el except

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1 — Aplicación: agrega tres libros y léelos ordenados por calificación, de mayor a menor (order_by(Book.rating.desc())).
  • 🟡 Reto 2 — Ampliación: busca los libros de un autor con where(Book.author == ...) y los que tienen calificación mayor a 8 (Book.rating > 8).
  • 🟠 Reto 3 — Desafío: intenta guardar un título repetido, atrapa IntegrityError, haz rollback() y muestra un mensaje claro.
  • 🔴 Reto 4 — Pensamiento computacional: escribe la misma consulta en SQL directo (SELECT ... WHERE ... ORDER BY ...) y con SQLAlchemy. Ejecuta print(db.select(Book).where(Book.rating > 8)) y compara el SQL que genera SQLAlchemy con el tuyo.

6. 🚀 Proyecto del día: la biblioteca virtual con base de datos

(Fuente: 450-build-a-sqlite-database-into-the-flask-website.md)

📝 Ejercicio original del curso

Integra la base de datos SQLite en el sitio del inicio del día, para que los libros se guarden y para aprovechar todo el CRUD:

  • Agregar libros en /add; al guardarlos con éxito, redirigir a la portada.
  • La portada muestra todos los libros de la base de datos.
  • Un enlace "Edit Rating" en cada <li> lleva a una página para cambiar la calificación; al pulsar "Change Rating", vuelve a la portada con la calificación nueva. Pista: piensa en cómo pasar el id del libro en la solicitud GET que muestra la página de edición (construcción de URL, o parámetros con nombre en la URL).
  • Un enlace "Delete" en cada <li> borra el libro y vuelve a la portada.

Problema y contexto

Quieres un registro de tus lecturas que no se borre cada vez que apagas el computador.

🔍 Antes de programar

  • ¿Cómo sabe la página de edición qué libro editar? Dos opciones: /edit/<int:id> o /edit?id=3 (request.args). ¿Cuál prefieres y por qué?
  • Después de editar o borrar, ¿por qué redirigir y no renderizar?
  • ¿Qué pasa si alguien visita /edit/999?

Requisitos funcionales

  1. Modelo Book con id, title (único, obligatorio), author y rating.
  2. Portada con todos los libros o "Library is empty.".
  3. /add (GET y POST) que guarda y redirige.
  4. Edición de la calificación por id.
  5. Borrado por id.

Restricciones técnicas

  • CRUD con SQLAlchemy 2 (db.select, get_or_404), sin Book.query.
  • url_for en todos los enlaces.
  • Títulos repetidos: mensaje claro, no error 500.

Planificación

  1. Comprender. 2. Entradas: formularios e ids. 3. Procesos: C, R, U, D. 4. Salidas: portada y formularios. 5. Dividir: una ruta por operación. 6. Diseñar las URL. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.

Construcción paso a paso

  • Versión 1: retos 1 a 5 de la lección 445 con la lista all_books.
  • Versión 2: modelo y create_all().
  • Versión 3: /add guarda en la base de datos; la portada lee de ella.
  • Versión 4: edición de la calificación.
  • Versión final: borrado y manejo de errores.

Pruebas

Caso Resultado esperado
Agregar 2 libros, reiniciar el servidor Siguen ahí
Base vacía "Library is empty."
Editar la calificación de un libro La portada muestra la nueva
/edit/999 404
Agregar un título repetido Mensaje claro; no error 500
Borrar un libro Desaparece de la portada y de DB Browser

📖 🔒 Borrar con un enlace: funciona, pero es mala idea (adaptación)

El curso pide un enlace (<a href="/delete?id=3">) para borrar. Funciona, pero un enlace hace una solicitud GET, y GET debería servir solo para ver (Día 60): los navegadores y algunas extensiones precargan enlaces, un buscador podría seguirlos, y otra página podría borrarte libros con un simple <img src="tu-sitio/delete?id=1">. La práctica correcta es borrar con un formulario POST (o con el método DELETE, que verás en el Día 66):

HTML
<form action="{{ url_for('delete', book_id=book.id) }}" method="post" style="display:inline">
    <button type="submit">Delete</button>
</form>

Haz primero la versión del curso y luego conviértela (Reto 2 del proyecto).

🧪 Prueba y depuración

Python
@app.route("/edit/<id>", methods=["GET", "POST"])
def edit(id):
    book = db.session.execute(db.select(Book).where(Book.id == id)).scalar()
    if request.method == "POST":
        book.rating = request.form["rating"]
        return render_template("index.html")
    return render_template("edit.html", book=book)

Encuentra cuatro problemas: falta int: en la ruta; si el id no existe, book es None (mejor get_or_404); falta db.session.commit(); y después de guardar se debería redirigir a la portada (además, index.html se renderiza sin la lista de libros).

⭐ Extensión del proyecto

  • Reto 1: ordena la portada por calificación y muestra el promedio de tus libros.
  • Reto 2: borra con un formulario POST en lugar de un enlace.
  • Reto 3: agrega la fecha en que terminaste cada libro (Mapped[date]) y muestra los leídos este año.

7. 🌟 Proyecto extra: el préstamo de balones del colegio

(Proyecto creado para esta adaptación; no existe en el curso original.)

Situación

En los descansos, el área de educación física presta balones, cuerdas y conos. Hoy lo anota en un cuaderno y siempre se pierde algo.

Requisitos

  1. Dos modelos: Implemento (id, nombre, tipo, estado: disponible/prestado/dañado) y Prestamo (id, id del implemento, curso, nombre del estudiante responsable, hora de salida, hora de devolución que puede quedar vacía).
  2. / muestra los implementos con su estado y un botón "Prestar" (solo si está disponible).
  3. Prestar crea un Prestamo y cambia el estado del implemento; "Devolver" registra la hora y lo deja disponible.
  4. /pendientes muestra los préstamos sin devolver, del más antiguo al más reciente.
  5. Todas las acciones que modifican datos se hacen con POST.

Decisiones que debes tomar (y justificar)

  • ¿Qué pasa si dos personas intentan prestar el mismo balón a la vez?
  • ¿Guardas el nombre completo del estudiante o solo el curso? ¿Por cuánto tiempo?

Casos de prueba

  • Prestar un balón ya prestado → mensaje de error.
  • Devolver → el implemento vuelve a "disponible" y el préstamo tiene hora de devolución.
  • Reiniciar el servidor → los préstamos siguen ahí.

8. 🏗️ Proyecto integrador del bloque avanzado — Etapa 5

(Proyecto creado para esta adaptación. Descripción completa en el tutorial del Día 70.)

La Plataforma del Club — Etapa 5 (hoy):

  1. Migra las actividades de actividades.csv (Día 62) a una tabla Actividad de SQLAlchemy. Escribe un pequeño script migrar.py que lea el CSV y cree los registros.
  2. Migra también las inscripciones.
  3. La portada y /actividades leen de la base de datos.
  4. Actualiza el README.md: dónde queda la base de datos (instance/) y cómo abrirla con DB Browser.

9. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué desaparecieron los libros al reiniciar el servidor?
  2. ¿Qué es una clave primaria? ¿Qué hacen NOT NULL y UNIQUE?
  3. ¿Qué es un ORM? ¿Qué se convierte en qué?
  4. ¿Qué significan las letras de CRUD?
  5. ¿Qué es la inyección SQL y cómo se evita?

Análisis de código

Python
libros = db.session.execute(db.select(Book)).scalars()
print(len(list(libros)))
return render_template("index.html", books=libros)

¿Qué verá el usuario en la portada? ¿Por qué?

Predicción

Python
with app.app_context():
    b = db.session.execute(db.select(Book).where(Book.title == "No existe")).scalar()
    print(b)
    print(b.title)

Depuración

"Agregué un libro, lo veo en la página, pero en DB Browser la tabla está vacía". Da dos causas posibles.

Modificación

Cambia /edit para que también permita cambiar el autor.

Creación

Escribe un modelo Estudiante (id, nombre, curso, promedio) y un script que cree 5 estudiantes y muestre los que tienen promedio mayor a 4.0, ordenados de mayor a menor.


10. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.

Actividad 1 — Código de otra época. Pide a una IA un CRUD con Flask-SQLAlchemy. ¿Usa Book.query.get() o db.Column sin Mapped? Compara con el Quickstart de Flask-SQLAlchemy 3.1 y actualízalo.

Actividad 2 — Inyección SQL. Pide a una IA un ejemplo de consulta con sqlite3 que use datos del usuario. ¿Usa parámetros ? o f-strings? Si usa f-strings, pídele que explique el riesgo y compara con la documentación de sqlite3 (sección sobre marcadores de posición).

Actividad 3 — ¿Dónde está mi base? Pregunta a una IA dónde guarda Flask-SQLAlchemy una base sqlite:///libros.db. Compruébalo en tu proyecto y en la documentación (sección Configuration).


11. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
448 Crear el registro con id: 1, author y review: 9.3 Falta title (obligatorio) y review no es un campo del modelo (verificado: IntegrityError y TypeError) Nota técnica de la sección 4
448 "Crea una tabla llamada books" Sin __tablename__, la tabla se llama book (verificado) Nota técnica de la sección 4
453 (Día 64) y 448 "Tendrás que crear la carpeta instance" Flask-SQLAlchemy 3.1 la crea sola (verificado) Nota técnica de la sección 4
447 Insertar la calificación como texto '9.3' SQLite la convierte a número por la afinidad de tipo (verificado); mejor sin comillas Sección 3
447 SQL escrito a mano Con datos del usuario hay que usar parámetros para evitar la inyección SQL Sección 3
449 all_books = result.scalars() El resultado solo se recorre una vez (verificado); .all() da una lista Tabla de la sección 5
450 Borrar con un enlace (GET) Las acciones que modifican datos deben usar POST (o DELETE) Sección 6
445 http://locahost:5000 Errata: localhost —

12. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Registro de libros leídos, LibraryThing, SQLite y CRUD 443 Sección 0 — CSV contra base de datos Sección 6
Proyecto de inicio, entorno virtual, requirements.txt, plantillas 444 Sección 1 Preparar el proyecto — Sección 6
Retos 1–5: <h1>, formulario, all_books, <ul>, biblioteca vacía 445 Sección 1 Retos 1–5 Preguntas antes de programar Sección 6
Reiniciar el servidor, datos perdidos, persistencia 446 Sección 2 Experimento del reinicio Memoria contra almacenamiento Sección 6
sqlite3, conexión, cursor, CREATE TABLE, clave primaria, NOT NULL, UNIQUE, DB Browser, INSERT, commit, errores comunes 447 Sección 3 Base de datos con SQL Inyección SQL Secciones 6 y 7
ORM, instalación, importaciones, reto de Flask-SQLAlchemy 3.1, URI, contexto de la aplicación 448 Sección 4 Reto de SQLAlchemy Notas sobre los datos del reto, nombre de la tabla e instance Secciones 6, 7 y 8
DeclarativeBase, modelo con Mapped, CRUD completo, scalars/scalar, get_or_404, métodos obsoletos 449 Sección 5 Ejemplos de CRUD Retos 1–4 de la sección 5 Secciones 6, 7 y 8
Requisitos del sitio: agregar y redirigir, portada, editar calificación, borrar 450 Sección 6 Proyecto del día Borrar con POST; extensiones Sección 6
(No existe en el curso original) — — — Préstamo de balones Sección 7
(No existe en el curso original) — — — Plataforma del Club, etapa 5 Sección 8

13. 📦 Evidencias de aprendizaje del Día 63

  • La biblioteca virtual con CRUD completo y la captura de la tabla en DB Browser.
  • La demostración del reinicio: capturas antes y después con los libros intactos.
  • El script de SQL directo con parámetros ? y la comparación con SQLAlchemy (Reto 4 de la sección 5).
  • El préstamo de balones (proyecto extra).
  • La etapa 5 de la Plataforma del Club con migrar.py.
  • Cuaderno de errores: tabla existente, VALUE contra VALUES, UNIQUE y NOT NULL, database is locked, falta de commit, contexto de la aplicación, scalars() recorrido dos veces, .scalar() que devuelve None, base de datos en instance/, PendingRollbackError, borrar con GET.