# Tutorial Guiado: SQLAlchemy con Flask Este tutorial te guiará paso a paso en el uso de SQLAlchemy con Flask para trabajar con bases de datos PostgreSQL. SQLAlchemy es un ORM (Object-Relational Mapping) que permite trabajar con bases de datos usando objetos Python en lugar de escribir SQL directo. ## ¿Qué es SQLAlchemy? SQLAlchemy es una biblioteca de Python que proporciona una abstracción sobre SQL, permitiendo trabajar con bases de datos relacionales usando objetos Python. En lugar de escribir queries SQL directamente, defines modelos como clases Python y SQLAlchemy se encarga de traducirlos a SQL. **Ventajas**: - Código más legible y mantenible - Protección contra inyección SQL - Independencia del motor de base de datos - Migraciones de esquema facilitadas ## Conceptos Fundamentales ### Modelo (Model) Un modelo es una clase Python que hereda de `db.Model` y representa una tabla en la base de datos. Cada atributo de la clase representa una columna en la tabla. ### Sesión (Session) La sesión es el contexto de trabajo con la base de datos. Gestiona las transacciones y mantiene el estado de los objetos que has cargado o modificado. ### Query Las queries permiten consultar la base de datos usando métodos de Python en lugar de SQL directo. ## Prerequisitos Esta sección establece los requisitos necesarios antes de comenzar el tutorial. Se requiere que PostgreSQL esté corriendo en Docker Compose y que verifiques la configuración básica del entorno antes de proceder con los pasos siguientes. ### PostgreSQL en Docker Compose Antes de comenzar, asegúrate de que tienes PostgreSQL corriendo en Docker Compose. Verifica que el servicio de base de datos está activo y saludable ejecutando: ```bash docker compose ps ``` Deberías ver el servicio `db` (o el nombre que hayas asignado a tu servicio de PostgreSQL) con estado "running" y healthcheck "healthy". Si el servicio no está corriendo, inícialo con: ```bash docker compose up -d db ``` Espera unos segundos a que el healthcheck confirme que PostgreSQL está listo para aceptar conexiones. ### Verificación de Servicios Para verificar que PostgreSQL está completamente operativo, puedes intentar conectarte directamente: ```bash docker compose exec db psql -U postgres -c "SELECT version();" ``` Si ves la versión de PostgreSQL, el servicio está funcionando correctamente. Si obtienes un error, revisa los logs del contenedor con `docker compose logs db`. ## Configuración de Variables de Entorno Esta sección explica cómo configurar las variables de entorno necesarias para conectarse a PostgreSQL y cómo Python carga estas variables usando la biblioteca `python-dotenv`. Comenzaremos explicando qué es dotenv y por qué lo utilizamos, seguido de los pasos para crear y configurar el archivo `.env`, y finalizando con la verificación de que todo está correctamente configurado. ### ¿Qué es python-dotenv? `python-dotenv` es una biblioteca de Python que permite cargar variables de entorno desde un archivo `.env` en lugar de tener que configurarlas manualmente en el sistema operativo o en el shell. Esta práctica es especialmente útil en desarrollo porque permite tener diferentes configuraciones para diferentes proyectos sin contaminar las variables de entorno globales del sistema. Cuando ejecutas `load_dotenv()` en tu código Python, la biblioteca busca un archivo `.env` en el directorio actual (o en directorios padre) y carga todas las variables definidas en ese archivo como si hubieran sido exportadas en el entorno. Esto significa que puedes usar `os.environ.get('DB_USER')` o `os.getenv('DB_USER')` para acceder a los valores definidos en el archivo `.env`. **Ventajas de usar dotenv**: - Separación de configuración por proyecto: cada proyecto puede tener su propio `.env` - Seguridad: el archivo `.env` puede estar en `.gitignore` para no versionar credenciales - Facilidad de desarrollo: no necesitas configurar variables de entorno del sistema - Portabilidad: el proyecto funciona en cualquier máquina que tenga el archivo `.env` configurado **Importante**: El archivo `.env` nunca debe versionarse en Git porque contiene información sensible como contraseñas. Por eso existe `.env.example` como plantilla sin valores reales. ### Paso 1: Crear archivo .env Si aún no tienes un archivo `.env`, créalo copiando desde el ejemplo: ```bash cp .env.example .env ``` Luego edita el archivo `.env` con tus valores reales. El archivo `.env.example` contiene comentarios explicativos sobre cada variable. ### Paso 2: Variables necesarias para el tutorial El tutorial requiere las siguientes variables de entorno que serán cargadas por `python-dotenv`: - `DB_USER`: Usuario de PostgreSQL (ejemplo: `postgres`) - `DB_PASSWORD`: Contraseña de PostgreSQL - `DB_NAME`: Nombre de la base de datos (ejemplo: `flaskapp`) - `DB_HOST`: Host de PostgreSQL - Usa `db` si ejecutas scripts dentro de contenedores Docker (nombre del servicio en docker-compose) - Usa `localhost` si ejecutas scripts desde tu máquina local (fuera de Docker) - `DB_PORT`: Puerto de PostgreSQL (por defecto: `5432`) ### Paso 3: Cómo funciona load_dotenv() En el código de configuración de Flask-SQLAlchemy que verás en el siguiente paso, encontrarás esta línea: ```python from dotenv import load_dotenv load_dotenv() # Carga las variables desde .env ``` Esta función busca el archivo `.env` en el directorio actual y en directorios padre, y carga todas las variables definidas en formato `CLAVE=valor`. Después de ejecutar `load_dotenv()`, puedes acceder a estas variables usando `os.environ.get('CLAVE')` o `os.getenv('CLAVE')`. **Ejemplo práctico**: Si tu archivo `.env` contiene: ``` DB_USER=postgres DB_PASSWORD=mi_password ``` Después de `load_dotenv()`, puedes hacer: ```python import os user = os.environ.get('DB_USER') # Retorna 'postgres' password = os.getenv('DB_PASSWORD') # Retorna 'mi_password' ``` ### Paso 4: Verificar configuración Puedes verificar que las variables están correctamente definidas en tu archivo `.env`: ```bash # Ver el contenido del archivo (sin exponerlo en producción) cat .env | grep DB_ ``` O desde Python, puedes crear un script de verificación. Crea el archivo `scripts/verify_env.py` en la raíz del proyecto (crea el directorio `scripts/` si no existe): ```python import os from dotenv import load_dotenv load_dotenv() required_vars = ['DB_USER', 'DB_PASSWORD', 'DB_NAME', 'DB_HOST', 'DB_PORT'] print("Verificando variables de entorno...") for var in required_vars: value = os.getenv(var) if value: # Ocultar contraseña por seguridad display_value = '***' if 'PASSWORD' in var else value print(f"✓ {var} = {display_value}") else: print(f"✗ {var} no está definida") ``` Ejecuta el script con: `python scripts/verify_env.py` ### Paso 5: Verificar conexión a PostgreSQL Antes de continuar, verifica que puedes conectarte a PostgreSQL usando las variables configuradas. Si ejecutas los scripts desde tu máquina local (no dentro de Docker), asegúrate de que `DB_HOST` esté configurado como `localhost`: ```bash # Cargar variables y verificar conexión (en Linux/Mac) source .env docker compose exec db psql -U ${DB_USER:-postgres} -d ${DB_NAME:-flaskapp} -c "SELECT version();" ``` Si ves la versión de PostgreSQL, la conexión está funcionando correctamente. Los valores por defecto (`:-postgres` y `:-flaskapp`) se usan si las variables no están definidas en el shell, pero `python-dotenv` las cargará cuando ejecutes los scripts Python. ## Estructura del Proyecto Esta sección describe la organización de archivos y directorios que utilizaremos a lo largo del tutorial. La estructura propuesta separa la configuración de la aplicación, los modelos de datos, y los scripts de prueba en directorios lógicos que facilitan el desarrollo y mantenimiento del proyecto. ``` proyecto/ ├── app/ │ ├── __init__.py # Configuración Flask-SQLAlchemy │ └── models.py # Modelos (Task) ├── scripts/ │ ├── 01_create_tables.py # Script para crear tablas │ ├── 02_test_create.py # Script para probar CREATE │ ├── 03_test_read.py # Script para probar READ │ ├── 04_test_update.py # Script para probar UPDATE │ └── 05_test_delete.py # Script para probar DELETE ├── .env.example # Plantilla variables de entorno ├── .env # Variables de entorno (no versionar) ├── requirements.txt # Dependencias Python └── README.md # Instrucciones del proyecto ``` El directorio `app/` contiene el código de la aplicación Flask y los modelos de datos. El directorio `scripts/` contiene scripts ejecutables independientes que permiten probar cada operación CRUD antes de integrarlas en la aplicación Flask. Los archivos `.env.example` y `.env` gestionan la configuración del proyecto, mientras que `requirements.txt` lista todas las dependencias necesarias. **Nota**: Asegúrate de crear los directorios `app/` y `scripts/` en la raíz del proyecto antes de crear los archivos dentro de ellos. No es necesario crear archivos `__init__.py` en estos directorios para este tutorial, ya que los scripts se ejecutan directamente desde la raíz del proyecto. ## Cómo usar este tutorial Esta sección proporciona una guía sobre cómo seguir el tutorial de manera efectiva. El enfoque está diseñado para que puedas ejecutar y probar cada operación paso a paso antes de integrarla en una aplicación Flask completa, facilitando el aprendizaje incremental y la depuración de problemas. ### Flujo de trabajo recomendado 1. **Configuración inicial**: Instala las dependencias y configura las variables de entorno siguiendo los pasos 1 y 2. 2. **Crear estructura básica**: Crea el directorio `app/` y los archivos `app/__init__.py` y `app/models.py` con el código proporcionado en los pasos 1 y 2. 3. **Crear tablas**: Crea el directorio `scripts/` y el archivo `scripts/01_create_tables.py`, luego ejecuta el script para crear las tablas en la base de datos. 4. **Probar operaciones CRUD**: Ejecuta los scripts en orden (`02_test_create.py`, `03_test_read.py`, `04_test_update.py`, `05_test_delete.py`) y verifica los resultados tanto en la consola como directamente en PostgreSQL usando `psql`. 5. **Integrar con Flask**: Una vez que hayas probado todas las operaciones, integra el código en rutas Flask siguiendo el paso 5. ### Verificación con psql Después de cada operación CRUD, el tutorial incluye comandos `psql` que puedes ejecutar para verificar directamente en PostgreSQL que los cambios se han realizado correctamente. Esto te permite entender cómo SQLAlchemy traduce las operaciones a SQL y te ayuda a depurar problemas si algo no funciona como se espera. ## Paso 1: Configuración Inicial Esta sección establece la base del proyecto configurando Flask-SQLAlchemy y las dependencias necesarias. Comenzaremos con la instalación de las bibliotecas requeridas, seguido de la configuración de la conexión a PostgreSQL usando variables de entorno, y finalizando con la inicialización de SQLAlchemy en la aplicación Flask. ### Instalación de Dependencias **Crear archivo**: `requirements.txt` en la raíz del proyecto Crea un archivo `requirements.txt` en la raíz del proyecto con el siguiente contenido: ```txt Flask==3.0.0 Flask-SQLAlchemy==3.1.1 psycopg2-binary==2.9.9 python-dotenv==1.0.0 ``` Instala las dependencias en tu entorno virtual: ```bash pip install -r requirements.txt ``` ### Configuración de Flask-SQLAlchemy **Crear directorio y archivo**: `app/__init__.py` Primero, crea el directorio `app/` en la raíz del proyecto si no existe. Luego crea el archivo `app/__init__.py` con la siguiente configuración: ```python from flask import Flask from flask_sqlalchemy import SQLAlchemy import os from dotenv import load_dotenv load_dotenv() # Crear instancia de SQLAlchemy db = SQLAlchemy() def create_app(): app = Flask(__name__) # Configurar connection string desde variables de entorno app.config['SQLALCHEMY_DATABASE_URI'] = ( f"postgresql://{os.environ.get('DB_USER')}:" f"{os.environ.get('DB_PASSWORD')}@" f"{os.environ.get('DB_HOST')}:" f"{os.environ.get('DB_PORT', '5432')}/" f"{os.environ.get('DB_NAME')}" ) # Desactivar tracking de modificaciones (ahorra recursos) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Inicializar SQLAlchemy con la aplicación db.init_app(app) return app ``` **Explicación de la configuración**: El parámetro `SQLALCHEMY_DATABASE_URI` construye la cadena de conexión a PostgreSQL usando el formato estándar `postgresql://usuario:contraseña@host:puerto/nombre_bd`. Todas las partes de esta cadena se obtienen de las variables de entorno cargadas por `load_dotenv()`. El parámetro `SQLALCHEMY_TRACK_MODIFICATIONS` se desactiva principalmente para ahorrar recursos, ya que habilitarlo hace que Flask-SQLAlchemy rastree todas las modificaciones en los objetos del modelo y envíe señales. Esta funcionalidad sirve para que la extensión pueda detectar automáticamente cambios en los modelos, pero suele ser innecesaria en la mayoría de los proyectos y genera una sobrecarga de memoria y procesamiento. Por eso, se recomienda dejarlo en `False` salvo que tengas una razón específica para utilizar ese sistema de señales. La función `db.init_app(app)` conecta la instancia de SQLAlchemy con la aplicación Flask, permitiendo que los modelos accedan a la base de datos a través del objeto `db`. ## Paso 2: Definir un Modelo Básico Esta sección explica cómo definir modelos en SQLAlchemy que representan tablas en la base de datos. Comenzaremos con la estructura básica de un modelo, seguido de la explicación de los tipos de columnas disponibles, y finalizando con métodos útiles para convertir los modelos a formatos como diccionarios JSON. ### Estructura de un Modelo **Crear archivo**: `app/models.py` Crea el archivo `app/models.py` dentro del directorio `app/` con el siguiente modelo: ```python from app import db from datetime import datetime class Task(db.Model): # Definir nombre de tabla (opcional, por defecto usa el nombre de la clase en minúsculas) __tablename__ = 'tasks' # Definir columnas id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) description = db.Column(db.Text, nullable=True) completed = db.Column(db.Boolean, default=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # Método para convertir a diccionario (útil para APIs JSON) def to_dict(self): return { 'id': self.id, 'title': self.title, 'description': self.description, 'completed': self.completed, 'created_at': self.created_at.isoformat() if self.created_at else None } # Método para representación en string (útil para debugging) def __repr__(self): return f'<Task {self.id}: {self.title}>' ``` **Explicación de tipos de columnas**: El tipo `db.Integer` representa números enteros y se usa típicamente para claves primarias y campos numéricos. `db.String(length)` define cadenas de texto con una longitud máxima especificada, útil para campos como títulos o nombres. `db.Text` permite almacenar texto largo sin límite de longitud, ideal para descripciones o contenido extenso. `db.Boolean` almacena valores True/False, mientras que `db.DateTime` maneja fechas y horas con precisión. `db.Float` se utiliza para números decimales cuando se requiere precisión de punto flotante. **Parámetros comunes de columnas**: El parámetro `primary_key=True` marca la columna como clave primaria, lo que significa que cada fila tendrá un valor único e identificador. `nullable=False` garantiza que la columna siempre tenga un valor, rechazando intentos de inserción con NULL. `default=valor` establece un valor por defecto que se usa cuando no se proporciona un valor explícito durante la inserción. `unique=True` asegura que todos los valores en la columna sean únicos en toda la tabla, útil para campos como emails o nombres de usuario. ## Paso 3: Crear las Tablas en la Base de Datos Esta sección explica cómo crear las tablas en PostgreSQL usando SQLAlchemy. Convertiremos el código de creación de tablas en un script ejecutable independiente que puedes ejecutar para inicializar la base de datos, seguido de instrucciones para verificar que las tablas se crearon correctamente usando `psql`. ### Script para Crear Tablas **Crear directorio y archivo**: `scripts/01_create_tables.py` Primero, crea el directorio `scripts/` en la raíz del proyecto si no existe. Luego crea el archivo `scripts/01_create_tables.py` con el siguiente contenido: ```python #!/usr/bin/env python3 """ Script para crear las tablas en la base de datos PostgreSQL Ejecutar: python scripts/01_create_tables.py """ import os import sys from dotenv import load_dotenv # Cargar variables de entorno load_dotenv() # Verificar que las variables necesarias están configuradas required_vars = ['DB_USER', 'DB_PASSWORD', 'DB_NAME', 'DB_HOST', 'DB_PORT'] missing_vars = [var for var in required_vars if not os.environ.get(var)] if missing_vars: print(f"❌ Error: Faltan las siguientes variables de entorno: {', '.join(missing_vars)}") print("Por favor, configura tu archivo .env antes de continuar.") sys.exit(1) from app import create_app, db from app.models import Task def main(): app = create_app() with app.app_context(): print("=" * 50) print("Creando tablas en la base de datos") print("=" * 50) try: # Crear todas las tablas definidas en los modelos db.create_all() print("\n✓ Tablas creadas correctamente") # Verificar que la tabla tasks existe inspector = db.inspect(db.engine) tables = inspector.get_table_names() if 'tasks' in tables: print(f"✓ Tabla 'tasks' verificada") print(f"\nColumnas de la tabla 'tasks':") columns = inspector.get_columns('tasks') for col in columns: print(f" - {col['name']}: {col['type']}") else: print("⚠ Advertencia: La tabla 'tasks' no se encontró") except Exception as e: print(f"\n❌ Error al crear tablas: {e}") sys.exit(1) print("\n" + "=" * 50) print("Verificación con psql:") print("=" * 50) print("\nEjecuta el siguiente comando para verificar en PostgreSQL:") db_user = os.getenv('DB_USER') db_name = os.getenv('DB_NAME') print(f"docker compose exec db psql -U {db_user} -d {db_name} -c \"\\d tasks\"") print("\nO desde psql directamente:") print("\\d tasks") if __name__ == '__main__': main() ``` **Explicación del script**: El script comienza verificando que todas las variables de entorno necesarias estén configuradas antes de intentar conectarse a la base de datos. Luego crea un contexto de aplicación Flask usando `app.app_context()`, que es necesario porque SQLAlchemy requiere un contexto de aplicación activo para realizar operaciones. La función `db.create_all()` crea todas las tablas definidas en los modelos que heredan de `db.Model`. Después de crear las tablas, el script verifica que la tabla `tasks` existe usando el inspector de SQLAlchemy, que permite examinar la estructura de la base de datos. Si ocurre algún error durante el proceso, se captura y se muestra un mensaje informativo antes de terminar la ejecución. **Importante**: `db.create_all()` solo crea tablas que no existen. No modifica tablas existentes ni actualiza su esquema. Para cambios en el esquema de tablas existentes, se deben usar migraciones con Flask-Migrate, que es un tema avanzado fuera del alcance de este tutorial básico. ### Verificación con psql Después de ejecutar el script, verifica que la tabla se creó correctamente: ```bash python scripts/01_create_tables.py ``` Luego ejecuta el comando psql sugerido en la salida del script: ```bash docker compose exec db psql -U <tu_usuario> -d <tu_base_datos> -c "\d tasks" ``` Deberías ver la estructura de la tabla `tasks` con todas sus columnas, tipos de datos y restricciones. Si ves un error, verifica que las variables de entorno estén correctamente configuradas y que PostgreSQL esté accesible. ## Paso 4: Operaciones CRUD Básicas Esta sección cubre las cuatro operaciones fundamentales de bases de datos: CREATE (crear), READ (leer), UPDATE (actualizar) y DELETE (eliminar). Cada operación se presenta como un script ejecutable independiente que puedes probar paso a paso, seguido de comandos `psql` para verificar los cambios directamente en PostgreSQL. Este enfoque te permite entender cómo SQLAlchemy traduce las operaciones Python a SQL antes de integrarlas en una aplicación Flask. ### 4.1 CREATE - Crear un Registro **Crear archivo**: `scripts/02_test_create.py` Crea el archivo `scripts/02_test_create.py` dentro del directorio `scripts/` con el siguiente contenido: ```python #!/usr/bin/env python3 """ Script para probar operaciones CREATE con SQLAlchemy Ejecutar: python scripts/02_test_create.py """ import os import sys from dotenv import load_dotenv load_dotenv() # Verificar variables de entorno required_vars = ['DB_USER', 'DB_PASSWORD', 'DB_NAME', 'DB_HOST', 'DB_PORT'] missing_vars = [var for var in required_vars if not os.environ.get(var)] if missing_vars: print(f"❌ Error: Faltan variables de entorno: {', '.join(missing_vars)}") sys.exit(1) from app import create_app, db from app.models import Task def main(): app = create_app() with app.app_context(): print("=" * 50) print("Probando operación CREATE") print("=" * 50) # Crear una nueva tarea new_task = Task( title='Aprender Docker', description='Completar práctica S11', completed=False ) print(f"\nTarea a crear:") print(f" Título: {new_task.title}") print(f" Descripción: {new_task.description}") print(f" Completada: {new_task.completed}") try: # Añadir a la sesión db.session.add(new_task) # Guardar cambios en la base de datos db.session.commit() print(f"\n✓ Tarea creada con ID: {new_task.id}") print(f" Fecha de creación: {new_task.created_at}") except Exception as e: db.session.rollback() print(f"\n❌ Error al crear tarea: {e}") sys.exit(1) print("\n" + "=" * 50) print("Verificación con psql:") print("=" * 50) db_user = os.getenv('DB_USER') db_name = os.getenv('DB_NAME') print(f"\nEjecuta el siguiente comando para verificar en PostgreSQL:") print(f"docker compose exec db psql -U {db_user} -d {db_name} -c \"SELECT * FROM tasks;\"") print("\nO desde psql directamente:") print("SELECT * FROM tasks;") print("\nDeberías ver el registro recién creado con su ID asignado automáticamente.") if __name__ == '__main__': main() ``` **Explicación de la operación CREATE**: El proceso de crear un registro en SQLAlchemy sigue tres pasos fundamentales. Primero, creas una instancia del modelo con los datos que deseas almacenar. En este caso, creamos un objeto `Task` con título, descripción y estado de completado. Segundo, añades el objeto a la sesión de la base de datos usando `db.session.add()`, que marca el objeto para ser insertado pero aún no ejecuta la operación. Tercero, confirmas los cambios con `db.session.commit()`, que ejecuta la inserción en la base de datos y asigna automáticamente un ID al objeto si tiene una clave primaria autoincremental. Si ocurre un error durante el commit, usamos `db.session.rollback()` para deshacer cualquier cambio pendiente y mantener la integridad de la transacción. **Pasos de la operación**: 1. Crear instancia del modelo con los datos 2. Añadir a la sesión con `db.session.add()` 3. Confirmar cambios con `db.session.commit()` ### 4.2 READ - Leer Registros **Crear archivo**: `scripts/03_test_read.py` Crea el archivo `scripts/03_test_read.py` dentro del directorio `scripts/` con el siguiente contenido: ```python #!/usr/bin/env python3 """ Script para probar operaciones READ con SQLAlchemy Ejecutar: python scripts/03_test_read.py """ import os import sys from dotenv import load_dotenv load_dotenv() required_vars = ['DB_USER', 'DB_PASSWORD', 'DB_NAME', 'DB_HOST', 'DB_PORT'] missing_vars = [var for var in required_vars if not os.environ.get(var)] if missing_vars: print(f"❌ Error: Faltan variables de entorno: {', '.join(missing_vars)}") sys.exit(1) from app import create_app, db from app.models import Task def main(): app = create_app() with app.app_context(): print("=" * 50) print("Probando operaciones READ") print("=" * 50) try: # Obtener todas las tareas print("\n1. Obtener todas las tareas:") all_tasks = Task.query.all() if all_tasks: for task in all_tasks: print(f" ID: {task.id}, Título: {task.title}, Completada: {task.completed}") else: print(" No hay tareas en la base de datos") # Obtener una tarea por ID print("\n2. Obtener tarea por ID (ID=1):") task = Task.query.get(1) if task: print(f" {task}") else: print(" No se encontró tarea con ID=1") # Filtrar tareas completadas print("\n3. Filtrar tareas completadas:") completed_tasks = Task.query.filter_by(completed=True).all() print(f" Encontradas {len(completed_tasks)} tareas completadas") for task in completed_tasks: print(f" - {task.title}") # Filtrar tareas pendientes print("\n4. Filtrar tareas pendientes:") pending_tasks = Task.query.filter(Task.completed == False).all() print(f" Encontradas {len(pending_tasks)} tareas pendientes") for task in pending_tasks: print(f" - {task.title}") # Obtener primera tarea pendiente print("\n5. Obtener primera tarea pendiente:") first_task = Task.query.filter_by(completed=False).first() if first_task: print(f" {first_task}") else: print(" No hay tareas pendientes") # Contar registros print("\n6. Contar total de tareas:") task_count = Task.query.count() print(f" Total: {task_count} tareas") except Exception as e: print(f"\n❌ Error al leer tareas: {e}") sys.exit(1) print("\n" + "=" * 50) print("Verificación con psql:") print("=" * 50) db_user = os.getenv('DB_USER') db_name = os.getenv('DB_NAME') print(f"\nEjecuta el siguiente comando para comparar resultados:") print(f"docker compose exec db psql -U {db_user} -d {db_name} -c \"SELECT id, title, completed FROM tasks WHERE completed = false;\"") print("\nLos resultados deberían coincidir con las tareas pendientes mostradas arriba.") if __name__ == '__main__': main() ``` **Explicación de métodos de query comunes**: El método `.all()` ejecuta la consulta y retorna todos los resultados como una lista de objetos del modelo. Es útil cuando necesitas procesar múltiples registros. `.first()` retorna solo el primer resultado que cumple las condiciones, o `None` si no hay resultados, ideal para casos donde esperas un único registro. `.get(id)` busca un registro específico por su clave primaria, retornando el objeto o `None` si no existe, siendo el método más eficiente para búsquedas por ID. `.filter_by()` permite filtrar por igualdad usando argumentos nombrados, siendo más simple y legible para condiciones básicas. `.filter()` permite condiciones más complejas usando operadores de comparación de SQLAlchemy, como `Task.completed == False` o `Task.title.like('%palabra%')`. El método `.count()` ejecuta una consulta COUNT en SQL y retorna el número total de registros que cumplen las condiciones, útil para estadísticas o paginación. ### 4.3 UPDATE - Actualizar un Registro **Crear archivo**: `scripts/04_test_update.py` Crea el archivo `scripts/04_test_update.py` dentro del directorio `scripts/` con el siguiente contenido: ```python #!/usr/bin/env python3 """ Script para probar operaciones UPDATE con SQLAlchemy Ejecutar: python scripts/04_test_update.py """ import os import sys from dotenv import load_dotenv load_dotenv() required_vars = ['DB_USER', 'DB_PASSWORD', 'DB_NAME', 'DB_HOST', 'DB_PORT'] missing_vars = [var for var in required_vars if not os.environ.get(var)] if missing_vars: print(f"❌ Error: Faltan variables de entorno: {', '.join(missing_vars)}") sys.exit(1) from app import create_app, db from app.models import Task def main(): app = create_app() with app.app_context(): print("=" * 50) print("Probando operaciones UPDATE") print("=" * 50) try: # Método 1: Actualizar objeto cargado print("\n1. Actualizar objeto cargado:") task = Task.query.get(1) if not task: print(" ❌ No se encontró tarea con ID=1. Ejecuta primero 02_test_create.py") sys.exit(1) print(f" Estado actual:") print(f" Título: {task.title}") print(f" Completada: {task.completed}") # Modificar atributos task.title = 'Título actualizado' task.completed = True # Guardar cambios db.session.commit() print(f" Estado después de actualizar:") print(f" Título: {task.title}") print(f" Completada: {task.completed}") print(" ✓ Actualización exitosa") # Método 2: Actualizar directamente con update print("\n2. Actualizar directamente con update():") # Primero revertimos el cambio anterior para demostrar Task.query.filter_by(id=1).update({'completed': False}) db.session.commit() updated_count = Task.query.filter_by(id=1).update({'completed': True}) db.session.commit() print(f" Registros actualizados: {updated_count}") print(" ✓ Actualización directa exitosa") # Verificar el resultado final task_final = Task.query.get(1) print(f"\n Estado final de la tarea ID=1:") print(f" Título: {task_final.title}") print(f" Completada: {task_final.completed}") except Exception as e: db.session.rollback() print(f"\n❌ Error al actualizar tarea: {e}") sys.exit(1) print("\n" + "=" * 50) print("Verificación con psql:") print("=" * 50) db_user = os.getenv('DB_USER') db_name = os.getenv('DB_NAME') print(f"\nEjecuta el siguiente comando para verificar:") print(f"docker compose exec db psql -U {db_user} -d {db_name} -c \"SELECT id, title, completed FROM tasks WHERE id = 1;\"") print("\nDeberías ver la tarea con los valores actualizados.") if __name__ == '__main__': main() ``` **Explicación de métodos de actualización**: SQLAlchemy ofrece dos formas principales de actualizar registros. El primer método consiste en cargar el objeto desde la base de datos, modificar sus atributos directamente como si fueran propiedades de Python normales, y luego hacer commit de la sesión. Este enfoque es intuitivo y permite aprovechar las características del modelo como validaciones o métodos personalizados. El segundo método usa `.update()` directamente sobre la query, lo que ejecuta una actualización masiva en SQL sin cargar los objetos en memoria. Este método es más eficiente cuando necesitas actualizar múltiples registros que cumplen ciertas condiciones, pero no dispara eventos del modelo ni ejecuta validaciones personalizadas. Ambos métodos requieren `db.session.commit()` para persistir los cambios en la base de datos. ### 4.4 DELETE - Eliminar un Registro **Crear archivo**: `scripts/05_test_delete.py` Crea el archivo `scripts/05_test_delete.py` dentro del directorio `scripts/` con el siguiente contenido: ```python #!/usr/bin/env python3 """ Script para probar operaciones DELETE con SQLAlchemy Ejecutar: python scripts/05_test_delete.py """ import os import sys from dotenv import load_dotenv load_dotenv() required_vars = ['DB_USER', 'DB_PASSWORD', 'DB_NAME', 'DB_HOST', 'DB_PORT'] missing_vars = [var for var in required_vars if not os.environ.get(var)] if missing_vars: print(f"❌ Error: Faltan variables de entorno: {', '.join(missing_vars)}") sys.exit(1) from app import create_app, db from app.models import Task def main(): app = create_app() with app.app_context(): print("=" * 50) print("Probando operaciones DELETE") print("=" * 50) try: # Contar tareas antes de eliminar count_before = Task.query.count() print(f"\nTareas en la base de datos antes: {count_before}") if count_before == 0: print("❌ No hay tareas para eliminar. Ejecuta primero 02_test_create.py") sys.exit(1) # Método 1: Eliminar objeto cargado print("\n1. Eliminar objeto cargado:") task = Task.query.get(1) if task: print(f" Tarea a eliminar: {task}") # Eliminar de la sesión db.session.delete(task) # Confirmar eliminación db.session.commit() print(" ✓ Tarea eliminada exitosamente") else: print(" ⚠ No se encontró tarea con ID=1") # Verificar que se eliminó task_after = Task.query.get(1) if task_after is None: print(" ✓ Verificación: La tarea ya no existe") else: print(" ❌ Error: La tarea aún existe") # Método 2: Eliminar directamente con delete() print("\n2. Eliminar directamente con delete():") # Primero creamos una tarea temporal para eliminar temp_task = Task(title='Tarea temporal', description='Para eliminar') db.session.add(temp_task) db.session.commit() temp_id = temp_task.id print(f" Tarea temporal creada con ID: {temp_id}") # Eliminar directamente deleted_count = Task.query.filter_by(id=temp_id).delete() db.session.commit() print(f" Registros eliminados: {deleted_count}") print(" ✓ Eliminación directa exitosa") # Contar tareas después de eliminar count_after = Task.query.count() print(f"\nTareas en la base de datos después: {count_after}") print(f"Total eliminadas en esta ejecución: {count_before - count_after}") except Exception as e: db.session.rollback() print(f"\n❌ Error al eliminar tarea: {e}") sys.exit(1) print("\n" + "=" * 50) print("Verificación con psql:") print("=" * 50) db_user = os.getenv('DB_USER') db_name = os.getenv('DB_NAME') print(f"\nEjecuta el siguiente comando para verificar:") print(f"docker compose exec db psql -U {db_user} -d {db_name} -c \"SELECT COUNT(*) FROM tasks;\"") print("\nEl conteo debería coincidir con el número mostrado arriba.") if __name__ == '__main__': main() ``` **Explicación de métodos de eliminación**: Al igual que con UPDATE, SQLAlchemy ofrece dos formas de eliminar registros. El primer método carga el objeto en memoria usando `.get()` o una query, luego lo marca para eliminación con `db.session.delete()`, y finalmente confirma con `db.session.commit()`. Este método es útil cuando necesitas ejecutar lógica adicional antes de eliminar, como validaciones o limpieza de relaciones. El segundo método usa `.delete()` directamente sobre la query, ejecutando una eliminación masiva en SQL sin cargar objetos. Este método retorna el número de registros eliminados y es más eficiente para operaciones en lote. Ambos métodos requieren commit para persistir la eliminación, y es buena práctica verificar que la eliminación fue exitosa consultando nuevamente la base de datos. ## Paso 5: Usar Modelos en Rutas Flask Esta sección muestra cómo integrar las operaciones CRUD que has probado en scripts independientes dentro de una aplicación Flask completa. Presentaremos un ejemplo de API REST completa que utiliza los modelos y operaciones que ya conoces, organizando el código en blueprints para mantener una estructura limpia y escalable. ### Ejemplo Completo de API REST **Crear archivo**: `app/routes.py` Crea el archivo `app/routes.py` dentro del directorio `app/` con el siguiente contenido: ```python from flask import Blueprint, request, jsonify from app import db from app.models import Task bp = Blueprint('tasks', __name__) # GET /tasks - Listar todas las tareas @bp.route('/tasks', methods=['GET']) def get_tasks(): tasks = Task.query.all() return jsonify([task.to_dict() for task in tasks]) # GET /tasks/<id> - Obtener una tarea específica @bp.route('/tasks/<int:task_id>', methods=['GET']) def get_task(task_id): task = Task.query.get_or_404(task_id) return jsonify(task.to_dict()) # POST /tasks - Crear una nueva tarea @bp.route('/tasks', methods=['POST']) def create_task(): data = request.get_json() # Validación básica if not data or 'title' not in data: return jsonify({'error': 'Title is required'}), 400 # Crear nueva tarea task = Task( title=data.get('title'), description=data.get('description', ''), completed=data.get('completed', False) ) db.session.add(task) db.session.commit() return jsonify(task.to_dict()), 201 # PUT /tasks/<id> - Actualizar una tarea @bp.route('/tasks/<int:task_id>', methods=['PUT']) def update_task(task_id): task = Task.query.get_or_404(task_id) data = request.get_json() # Actualizar campos si están presentes if 'title' in data: task.title = data['title'] if 'description' in data: task.description = data['description'] if 'completed' in data: task.completed = data['completed'] db.session.commit() return jsonify(task.to_dict()) # DELETE /tasks/<id> - Eliminar una tarea @bp.route('/tasks/<int:task_id>', methods=['DELETE']) def delete_task(task_id): task = Task.query.get_or_404(task_id) db.session.delete(task) db.session.commit() return '', 204 ``` **Actualizar archivo**: `app/__init__.py` Ahora actualiza el archivo `app/__init__.py` que creaste anteriormente para registrar el blueprint. Reemplaza el contenido completo del archivo con el siguiente código: ```python from flask import Flask from flask_sqlalchemy import SQLAlchemy import os from dotenv import load_dotenv load_dotenv() # Crear instancia de SQLAlchemy db = SQLAlchemy() def create_app(): app = Flask(__name__) # Configurar connection string desde variables de entorno app.config['SQLALCHEMY_DATABASE_URI'] = ( f"postgresql://{os.environ.get('DB_USER')}:" f"{os.environ.get('DB_PASSWORD')}@" f"{os.environ.get('DB_HOST')}:" f"{os.environ.get('DB_PORT', '5432')}/" f"{os.environ.get('DB_NAME')}" ) # Desactivar tracking de modificaciones (ahorra recursos) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Inicializar SQLAlchemy con la aplicación db.init_app(app) # Registrar blueprints from app.routes import bp app.register_blueprint(bp, url_prefix='/api') return app ``` **Explicación de la integración**: Los endpoints REST utilizan exactamente las mismas operaciones CRUD que probaste en los scripts anteriores, pero ahora están envueltos en funciones de ruta Flask que manejan las peticiones HTTP. El método `get_or_404()` es una extensión de Flask-SQLAlchemy que automáticamente retorna un error 404 si el registro no existe, simplificando el manejo de errores. El método `to_dict()` que definiste en el modelo facilita la serialización a JSON para las respuestas de la API. Los blueprints permiten organizar las rutas en módulos separados, mejorando la mantenibilidad del código a medida que la aplicación crece. ## Paso 6: Manejo de Errores y Transacciones Esta sección cubre el manejo robusto de errores y el uso de transacciones en SQLAlchemy. Comenzaremos con el manejo básico de errores usando try-except y rollback, seguido de ejemplos de transacciones que garantizan atomicidad en operaciones múltiples. ### Rollback en Caso de Error Cuando ocurre un error durante una operación de base de datos, es importante deshacer los cambios pendientes para mantener la integridad de los datos: ```python from app import db from app.models import Task try: task = Task(title='Nueva tarea') db.session.add(task) db.session.commit() except Exception as e: db.session.rollback() # Deshacer cambios si hay error print(f"Error: {e}") ``` **Explicación del manejo de errores**: El bloque `try-except` captura cualquier excepción que ocurra durante la operación de base de datos. Si el commit falla por cualquier razón (violación de restricciones, error de conexión, etc.), `db.session.rollback()` deshace todos los cambios pendientes en la transacción actual, restaurando el estado de la base de datos al punto anterior al inicio de la transacción. Esto es crucial para mantener la consistencia de los datos y evitar estados parciales que podrían causar problemas posteriores. ### Uso de Transacciones Las transacciones permiten agrupar múltiples operaciones que deben ejecutarse todas juntas o ninguna: ```python from app import db from app.models import Task # Crear múltiples tareas en una transacción try: task1 = Task(title='Tarea 1') task2 = Task(title='Tarea 2') task3 = Task(title='Tarea 3') db.session.add_all([task1, task2, task3]) db.session.commit() # Todas se guardan o ninguna except Exception as e: db.session.rollback() print(f"Error: {e}") ``` **Explicación de transacciones**: El método `db.session.add_all()` permite añadir múltiples objetos a la sesión en una sola llamada, preparándolos todos para ser insertados en la misma transacción. Cuando ejecutas `db.session.commit()`, todas las operaciones se ejecutan como una unidad atómica: si alguna falla, todas se revierten automáticamente mediante el rollback en el bloque except. Esta propiedad de atomicidad es fundamental en bases de datos y garantiza que no queden datos parciales si algo sale mal durante el proceso. ## Ejercicios Prácticos ### Ejercicio 1: Modelo de Usuario Crea un modelo `User` con los siguientes campos: - `id` (Integer, primary key) - `username` (String, único, no nulo) - `email` (String, único, no nulo) - `created_at` (DateTime, por defecto ahora) ### Ejercicio 2: Queries Avanzadas Usando el modelo `Task`, escribe queries para: 1. Obtener todas las tareas completadas ordenadas por fecha de creación (más recientes primero) 2. Obtener tareas cuyo título contenga una palabra específica 3. Obtener las 5 tareas más recientes ### Ejercicio 3: API Completa Crea endpoints para: - Listar usuarios - Crear usuario - Actualizar usuario - Eliminar usuario ## Recursos Adicionales - [Flask-SQLAlchemy Documentation](https://flask-sqlalchemy.palletsprojects.com/) - [SQLAlchemy Documentation](https://www.sqlalchemy.org/) - [SQLAlchemy Tutorial](https://docs.sqlalchemy.org/en/20/tutorial/)