Juan Diego Andrés PRADA··RAMÍREZ Entrar
Lección 4 de 7

Base de datos

¿Qué es?

Una base de datos es un lugar donde tus datos se guardan de forma permanente, en disco, y no desaparecen al cerrar el programa. A eso se le llama persistencia. En este módulo usamos SQLModel, una librería del mismo autor de FastAPI que combina Pydantic (validación) y SQLAlchemy (acceso a la base) en una sola clase. Trabajamos sobre SQLite, una base que vive en un simple archivo y no requiere instalar nada extra.

¿Cómo funciona?

Defines tus modelos como tablas marcándolos con table=True, y SQLModel crea esas tablas en el archivo por ti. Cada operación (crear, leer, actualizar, borrar: el CRUD) ocurre dentro de una sesión, que abres por petición y cierras al terminar. Escribes con session.add y session.commit, y lees con session.get o select. Las claves foráneas y Relationship conectan tablas entre sí, por ejemplo un estudiante con todas sus notas.

¿Para qué sirve?

Sin base de datos, todo lo que tu API guarda se pierde al reiniciar el servidor; con ella, los datos sobreviven. Es lo que separa una demo de juguete de una aplicación real que la gente puede usar día a día. En el proyecto final, «API para tus herramientas», esto hace que las notas y demás registros dejen de borrarse al cerrar el programa y queden guardados de verdad.

Las listas en memoria se borran al reiniciar el servidor. Para persistir datos usaremos SQLModel, una librería del mismo autor de FastAPI que combina Pydantic (validación) y SQLAlchemy (acceso a base de datos) en una sola clase. Aprenderás a definir tablas, hacer un CRUD completo y modelar relaciones. Con esto, el proyecto «API para tus herramientas» dejará de perder las notas al cerrar el programa.

Configuración

pip install sqlmodel

Definimos el motor y una función para crear las tablas. Usamos SQLite, que guarda todo en un archivo y no requiere instalar nada:

from sqlmodel import SQLModel, create_engine, Session

DATABASE_URL = "sqlite:///./notas.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})


def crear_tablas():
 SQLModel.metadata.create_all(engine)

El engine es la conexión al archivo. connect_args={"check_same_thread": False} es necesario solo en SQLite por cómo FastAPI maneja hilos.

Modelos como tablas

Un modelo SQLModel con table=True se convierte en una tabla real:

from sqlmodel import SQLModel, Field


class Nota(SQLModel, table=True):
 id: int | None = Field(default=None, primary_key=True)
 estudiante: str
 valor: float
 materia: str = "General"

id es la clave primaria; la marcamos opcional con default=None porque la base de datos la asigna sola al insertar. Como SQLModel hereda de Pydantic, esta misma clase valida los datos de entrada.

Conviene separar el modelo de tabla del modelo de entrada. Un NotaCrear(SQLModel) sin table=True y sin id evita que un cliente intente fijar el id a mano. Es el mismo principio de response_model del módulo anterior, aplicado a la persistencia.

La sesión como dependencia

Cada operación de base de datos ocurre dentro de una sesión. La inyectamos con el sistema de dependencias de FastAPI, Depends:

from fastapi import Depends
from sqlmodel import Session


def get_session():
 with Session(engine) as session:
 yield session

El yield entrega la sesión al endpoint y la cierra al terminar, pase lo que pase. Veremos Depends a fondo en el módulo de autenticación; aquí lo usamos para no abrir conexiones a mano.

CRUD completo

CRUD son las cuatro operaciones: Create, Read, Update, Delete.

from fastapi import FastAPI, HTTPException
from sqlmodel import select

app = FastAPI(title="API de notas")


@app.on_event("startup")
def on_startup():
 crear_tablas()


@app.post("/notas", response_model=Nota, status_code=201)
def crear(nota: Nota, session: Session = Depends(get_session)):
 session.add(nota)
 session.commit()
 session.refresh(nota) # recarga el id asignado por la base
 return nota


@app.get("/notas", response_model=list[Nota])
def listar(session: Session = Depends(get_session)):
 return session.exec(select(Nota)).all()


@app.get("/notas/{nota_id}", response_model=Nota)
def obtener(nota_id: int, session: Session = Depends(get_session)):
 nota = session.get(Nota, nota_id)
 if not nota:
 raise HTTPException(404, "Nota no encontrada")
 return nota


@app.patch("/notas/{nota_id}", response_model=Nota)
def actualizar(nota_id: int, valor: float, session: Session = Depends(get_session)):
 nota = session.get(Nota, nota_id)
 if not nota:
 raise HTTPException(404, "Nota no encontrada")
 nota.valor = valor
 session.add(nota)
 session.commit()
 session.refresh(nota)
 return nota


@app.delete("/notas/{nota_id}", status_code=204)
def borrar(nota_id: int, session: Session = Depends(get_session)):
 nota = session.get(Nota, nota_id)
 if not nota:
 raise HTTPException(404, "Nota no encontrada")
 session.delete(nota)
 session.commit()

Fíjate en el patrón: session.add + session.commit para escribir; session.get o select para leer; session.refresh para recuperar valores que genera la base. El 204 en DELETE significa «hecho, sin contenido que devolver».

Relaciones

Las claves foráneas conectan tablas. Un estudiante tiene muchas notas:

from sqlmodel import Relationship


class Estudiante(SQLModel, table=True):
 id: int | None = Field(default=None, primary_key=True)
 nombre: str
 notas: list["Nota"] = Relationship(back_populates="estudiante")


class Nota(SQLModel, table=True):
 id: int | None = Field(default=None, primary_key=True)
 valor: float
 estudiante_id: int | None = Field(default=None, foreign_key="estudiante.id")
 estudiante: Estudiante | None = Relationship(back_populates="notas")

foreign_key="estudiante.id" crea el vínculo en la base; Relationship te deja navegarlo desde Python: mi_estudiante.notas devuelve la lista, y mi_nota.estudiante el dueño. back_populates mantiene ambos lados sincronizados.

Ejemplo guiado: correr el CRUD

  1. Junta el engine, los modelos y los endpoints en main.py.
  2. Corre fastapi dev main.py. Al arrancar se crea notas.db y las tablas.
  3. En /docs, POST /notas con {"estudiante": "Ana", "valor": 4.5}. Apunta el id devuelto.
  4. Reinicia el servidor (Ctrl+C y de nuevo fastapi dev). Llama GET /notas: la nota sigue ahí. Eso es persistencia.
  5. Prueba PATCH /notas/1?valor=3.8 y luego DELETE /notas/1.

No reutilices una misma Session entre varias peticiones ni entre hilos. Por eso get_session crea una por petición y la cierra con el with. Una sesión compartida produce errores intermitentes difíciles de rastrear.

Tech English: primary key = clave primaria. foreign key = clave foránea. commit = confirmar los cambios. query = consulta. migration = cambio versionado del esquema de la base.

Ejercicios

  1. Persistir el sistema de notas. Convierte la API de notas en memoria a SQLModel con CRUD completo (los cinco endpoints). Evalúa: ¿cada operación usa el método HTTP correcto? ¿el 404 aparece en lectura, actualización y borrado? ¿el DELETE responde 204?

  2. Cursos y estudiantes. Modela una relación uno-a-muchos entre Curso y Estudiante, con POST /cursos, POST /cursos/{curso_id}/estudiantes y GET /cursos/{curso_id} que devuelva el curso con sus estudiantes anidados. Evalúa el diseño de las rutas (anidamiento del recurso) y la validez de la clave foránea. Esta estructura sostendrá el sistema de notas del proyecto final.

Tu progreso se guarda en este navegador. Inicia sesión para guardarlo en tu cuenta y verlo desde cualquier dispositivo.