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
titley aparecereviewen lugar derating), que la tabla se llamabooky nobookssi no se indica, que Flask-SQLAlchemy crea sola la carpetainstance, que el resultado descalars()solo se puede recorrer una vez, y el comportamiento deget_or_404.
Tabla de contenidos
- Antes de empezar: la biblioteca que olvidaba sus libros
- El sitio sin base de datos
- ¿Qué pasa al reiniciar el servidor?
- SQLite y SQL directo
- SQLAlchemy: la base de datos como clases de Python
- CRUD con SQLAlchemy
- 🚀 Proyecto del día: la biblioteca virtual con base de datos
- 🌟 Proyecto extra: el préstamo de balones del colegio
- 🏗️ Proyecto integrador del bloque avanzado — Etapa 5
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 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_fory 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:
- En la ruta
/, un<h1>"My Library" y un enlace "Add New Book". - En
/add, un formulario con título, autor y calificación, y un botón "Add Book". - Al enviar el formulario, el libro se agrega como diccionario a la lista
all_books:
all_books = [
{
"title": "Harry Potter",
"author": "J. K. Rowling",
"rating": 9,
}
]- La portada muestra cada libro como
<li>dentro de un<ul>. - 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_templateo rediriges conredirect(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:
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:
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:
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í:
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"):
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.
- Comenta el código de
sqlite3. - Instala
flask,SQLAlchemyyflask_sqlalchemydesderequirements.txt. - Importa:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, FloatReto, con la documentación de Flask-SQLAlchemy 3.1:
- una base de datos SQLite
new-books-collection.db(inicializa la aplicación); - una tabla
booksconid,title,authoryrating, 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.- una base de datos SQLite
📖 La solución (lección 449, verificada)
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
DeclarativeBaseal crearSQLAlchemy. - 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 conwith.
🛠️ Nota técnica (verificado): el reto tiene tres detalles que no coinciden:
- Los datos del registro no incluyen
title, que esNOT NULL: al guardarlo apareceIntegrityError: NOT NULL constraint failed: book.title. Agregatitle="Harry Potter".- Dice
review: 9.3, pero el campo se llamarating:Book(review=9.3)lanzaTypeError: 'review' is an invalid keyword argument for Book.- 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.dbcrea la base de datos en la carpetainstancedel proyecto, no junto amain.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.
# 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 (unResult);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, hazrollback()y muestra un mensaje claro. - 🔴 Reto 4 — Pensamiento computacional: escribe la misma consulta en SQL directo (
SELECT ... WHERE ... ORDER BY ...) y con SQLAlchemy. Ejecutaprint(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
- Modelo
Bookconid,title(único, obligatorio),authoryrating. - Portada con todos los libros o "Library is empty.".
/add(GET y POST) que guarda y redirige.- Edición de la calificación por id.
- Borrado por id.
Restricciones técnicas
- CRUD con SQLAlchemy 2 (
db.select,get_or_404), sinBook.query. url_foren todos los enlaces.- Títulos repetidos: mensaje claro, no error 500.
Planificación
- 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:
/addguarda 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):
<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
@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
- Dos modelos:
Implemento(id, nombre, tipo, estado: disponible/prestado/dañado) yPrestamo(id, id del implemento, curso, nombre del estudiante responsable, hora de salida, hora de devolución que puede quedar vacía). /muestra los implementos con su estado y un botón "Prestar" (solo si está disponible).- Prestar crea un
Prestamoy cambia el estado del implemento; "Devolver" registra la hora y lo deja disponible. /pendientesmuestra los préstamos sin devolver, del más antiguo al más reciente.- 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):
- Migra las actividades de
actividades.csv(Día 62) a una tablaActividadde SQLAlchemy. Escribe un pequeño scriptmigrar.pyque lea el CSV y cree los registros. - Migra también las inscripciones.
- La portada y
/actividadesleen de la base de datos. - Actualiza el
README.md: dónde queda la base de datos (instance/) y cómo abrirla con DB Browser.
9. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Por qué desaparecieron los libros al reiniciar el servidor?
- ¿Qué es una clave primaria? ¿Qué hacen
NOT NULLyUNIQUE? - ¿Qué es un ORM? ¿Qué se convierte en qué?
- ¿Qué significan las letras de CRUD?
- ¿Qué es la inyección SQL y cómo se evita?
Análisis de código
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
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,
VALUEcontraVALUES,UNIQUEyNOT NULL,database is locked, falta decommit, contexto de la aplicación,scalars()recorrido dos veces,.scalar()que devuelveNone, base de datos eninstance/,PendingRollbackError, borrar con GET.