IF0100 - Programación OO II

Unidad 3: Desarrollo Web con FastAPI

Clase 12: Persistencia con SQLAlchemy

Jueves, 23 de abril de 2026 Semana 12 - Jueves (60 minutos) 30 min Teoría 30 min Práctica

E4: Lab Persistencia - Hoy (Jueves 23/04/2026)

Objetivos de Aprendizaje

  • Conectar FastAPI con base de datos SQLite
  • Crear modelos con SQLAlchemy ORM
  • Implementar operaciones CRUD
  • Usar dependencias para sesiones de BD

SQLAlchemy (10 min)

SQLAlchemy es un ORM (Object Relational Mapper) para Python.

Instalación

pip install sqlalchemy

Conexión a SQLite

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# Crear motor de base de datos
SQLALCHEMY_DATABASE_URL = "sqlite:///./taskflow.db"
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, 
    connect_args={"check_same_thread": False}
)

# Crear sesión
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Base para modelos
Base = declarative_base()

# Función para obtener sesión
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Modelos ORM (20 min)

from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from database import Base

class TareaDB(Base):
    __tablename__ = "tareas"
    
    id = Column(Integer, primary_key=True, index=True)
    titulo = Column(String, nullable=False)
    descripcion = Column(String)
    completada = Column(Boolean, default=False)
    prioridad = Column(Integer, default=1)
    fecha_creacion = Column(DateTime, server_default=func.now())

class UsuarioDB(Base):
    __tablename__ = "usuarios"
    
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, nullable=False)
    email = Column(String, unique=True, nullable=False)
    password_hash = Column(String, nullable=False)
    activo = Column(Boolean, default=True)

# Crear tablas
Base.metadata.create_all(bind=engine)

Pydantic Models para API

from pydantic import BaseModel
from typing import Optional
from datetime import datetime

class TareaBase(BaseModel):
    titulo: str
    descripcion: Optional[str] = None
    prioridad: int = 1

class TareaCreate(TareaBase):
    pass

class Tarea(TareaBase):
    id: int
    completada: bool
    fecha_creacion: datetime
    
    class Config:
        from_attributes = True  # Permite convertir desde ORM

Operaciones CRUD (25 min)

from sqlalchemy.orm import Session
from models import TareaDB

# CREATE
def create_tarea(db: Session, tarea: TareaCreate):
    db_tarea = TareaDB(**tarea.model_dump())
    db.add(db_tarea)
    db.commit()
    db.refresh(db_tarea)
    return db_tarea

# READ
def get_tarea(db: Session, tarea_id: int):
    return db.query(TareaDB).filter(TareaDB.id == tarea_id).first()

def get_tareas(db: Session, skip: int = 0, limit: int = 100):
    return db.query(TareaDB).offset(skip).limit(limit).all()

# UPDATE
def update_tarea(db: Session, tarea_id: int, tarea_update: TareaCreate):
    db_tarea = db.query(TareaDB).filter(TareaDB.id == tarea_id).first()
    if db_tarea:
        for key, value in tarea_update.model_dump().items():
            setattr(db_tarea, key, value)
        db.commit()
        db.refresh(db_tarea)
    return db_tarea

# DELETE
def delete_tarea(db: Session, tarea_id: int):
    db_tarea = db.query(TareaDB).filter(TareaDB.id == tarea_id).first()
    if db_tarea:
        db.delete(db_tarea)
        db.commit()
        return True
    return False

FastAPI + Base de Datos (15 min)

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from database import get_db, engine
from models import Base, TareaDB
from schemas import Tarea, TareaCreate
from crud import create_tarea, get_tarea, get_tareas, update_tarea, delete_tarea

# Crear tablas
Base.metadata.create_all(bind=engine)

app = FastAPI(title="TaskFlow con BD")

@app.post("/tareas", response_model=Tarea)
def crear_tarea(tarea: TareaCreate, db: Session = Depends(get_db)):
    return create_tarea(db=db, tarea=tarea)

@app.get("/tareas", response_model=list[Tarea])
def listar_tareas(
    skip: int = 0, 
    limit: int = 100, 
    db: Session = Depends(get_db)
):
    return get_tareas(db, skip=skip, limit=limit)

@app.get("/tareas/{tarea_id}", response_model=Tarea)
def obtener_tarea(tarea_id: int, db: Session = Depends(get_db)):
    db_tarea = get_tarea(db, tarea_id=tarea_id)
    if db_tarea is None:
        raise HTTPException(status_code=404, detail="Tarea no encontrada")
    return db_tarea

@app.put("/tareas/{tarea_id}", response_model=Tarea)
def actualizar_tarea(
    tarea_id: int, 
    tarea: TareaCreate, 
    db: Session = Depends(get_db)
):
    db_tarea = update_tarea(db, tarea_id=tarea_id, tarea_update=tarea)
    if db_tarea is None:
        raise HTTPException(status_code=404, detail="Tarea no encontrada")
    return db_tarea

@app.delete("/tareas/{tarea_id}")
def eliminar_tarea(tarea_id: int, db: Session = Depends(get_db)):
    success = delete_tarea(db, tarea_id=tarea_id)
    if not success:
        raise HTTPException(status_code=404, detail="Tarea no encontrada")
    return {"message": "Tarea eliminada"}

Estructura de Archivos

taskflow/
├── main.py           # FastAPI app
├── database.py       # Conexión SQLAlchemy
├── models.py         # Modelos ORM
├── schemas.py        # Pydantic models
├── crud.py          # Operaciones CRUD
└── taskflow.db      # Base de datos SQLite

Ejercicio: Usuarios con BD (10 min)

Implementa CRUD completo para el modelo Usuario con base de datos.

Requisitos

  • Tabla usuarios con: id, username, email, password_hash, activo
  • Validar username único antes de crear
  • Validar email único antes de crear
  • No retornar password_hash en las respuestas
# schemas.py
class UsuarioBase(BaseModel):
    username: str
    email: str

class UsuarioCreate(UsuarioBase):
    password: str

class Usuario(UsuarioBase):
    id: int
    activo: bool
    
    class Config:
        from_attributes = True

# crud.py
def create_usuario(db: Session, usuario: UsuarioCreate):
    # Verificar si username existe
    if db.query(UsuarioDB).filter(UsuarioDB.username == usuario.username).first():
        raise HTTPException(400, "Username ya existe")
    
    # Crear usuario (hash password en producción)
    db_usuario = UsuarioDB(
        username=usuario.username,
        email=usuario.email,
        password_hash=f"hash_{usuario.password}"  # Simplificado
    )
    db.add(db_usuario)
    db.commit()
    db.refresh(db_usuario)
    return db_usuario
Evaluación 3: Entregar API completa con base de datos SQLite + tests + cobertura 80%.

Resumen Unidad 3

FastAPI + Pydantic + SQLAlchemy

  • FastAPI: Framework para APIs rápidas
  • Pydantic: Validación de datos
  • Depends: Inyección de dependencias
  • TestClient: Testing de APIs
  • SQLAlchemy: ORM para base de datos
🎉 Felicitaciones! Has completado la Unidad 3. Ahora puedes construir APIs completas con Python.
← Anterior: Testing FastAPI
Clase 15 de 25
Siguiente: Archivos JSON/CSV →