fixes with SQL Alchemy to avoid precaching

This commit is contained in:
2026-05-06 12:05:12 -06:00
parent 57fa3b7944
commit fea63bb553
2 changed files with 98 additions and 40 deletions

View File

@@ -2,6 +2,7 @@ from application.ports.user_repository import UserRepository
from domain.users import User
from infrastructure.adapters.persistence.models import UserModel
from infrastructure.adapters.persistence.db import SessionLocal
from sqlalchemy.orm import Session
from typing import List, Optional
import logging
@@ -10,11 +11,24 @@ logger = logging.getLogger(__name__)
class UserRepositorySQL(UserRepository):
"""Implementación del repositorio de Usuarios usando SQLAlchemy (MySQL)"""
def __init__(self, db_session=None):
self.db = db_session or SessionLocal()
def __init__(self, db_session: Session = None):
# FIXED: la sesión se guarda solo si se inyecta explícitamente.
# Cuando db_session es None, cada método abre y cierra su propia
# sesión, eliminando la caché de primer nivel entre requests.
self._injected_session = db_session
def _get_session(self):
"""
Devuelve la sesión inyectada si existe, o crea una nueva.
El caller es responsable de cerrarla cuando no fue inyectada.
"""
if self._injected_session is not None:
return self._injected_session, False # (sesión, es_propia)
return SessionLocal(), True
def save(self, user: User) -> User:
"""Guarda un nuevo usuario con manejo de transacciones"""
db, is_own = self._get_session()
try:
db_user = UserModel(
nombre=user.nombre,
@@ -29,60 +43,84 @@ class UserRepositorySQL(UserRepository):
biografia=user.biografia,
is_admin=user.is_admin
)
self.db.add(db_user)
self.db.commit()
self.db.refresh(db_user)
db.add(db_user)
db.commit()
db.refresh(db_user)
logger.info(f"Usuario guardado exitosamente: {db_user.user_id}")
return self._to_domain(db_user)
except Exception as e:
self.db.rollback()
db.rollback()
logger.error(f"Error al guardar usuario: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def find_by_id(self, user_id: int) -> Optional[User]:
"""Obtiene un usuario por ID"""
db, is_own = self._get_session()
try:
db_user = self.db.query(UserModel).filter(UserModel.user_id == user_id).first()
db_user = db.query(UserModel).filter(UserModel.user_id == user_id).first()
if db_user:
return self._to_domain(db_user)
return None
except Exception as e:
logger.error(f"Error al buscar usuario por ID {user_id}: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def find_by_email(self, email: str) -> Optional[User]:
"""Obtiene un usuario por email"""
db, is_own = self._get_session()
try:
db_user = self.db.query(UserModel).filter(UserModel.email == email).first()
db_user = db.query(UserModel).filter(UserModel.email == email).first()
if db_user:
return self._to_domain(db_user)
return None
except Exception as e:
logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def find_by_email_with_password(self, email: str) -> Optional[UserModel]:
"""Obtiene un usuario por email incluyendo el hash de contraseña (para autenticación)"""
db, is_own = self._get_session()
try:
db_user = self.db.query(UserModel).filter(UserModel.email == email).first()
db_user = db.query(UserModel).filter(UserModel.email == email).first()
# FIXED: si la sesión es propia, expunge para poder usar el objeto
# fuera de la sesión sin que SQLAlchemy intente lazy-load nada.
if db_user and is_own:
db.expunge(db_user)
return db_user
except Exception as e:
logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def find_all(self) -> List[User]:
"""Obtiene todos los usuarios"""
db, is_own = self._get_session()
try:
db_users = self.db.query(UserModel).all()
db_users = db.query(UserModel).all()
return [self._to_domain(user) for user in db_users]
except Exception as e:
logger.error(f"Error al obtener todos los usuarios: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def update(self, user: User) -> User:
"""Actualiza un usuario con manejo de transacciones"""
db, is_own = self._get_session()
try:
db_user = self.db.query(UserModel).filter(UserModel.user_id == user.user_id).first()
db_user = db.query(UserModel).filter(UserModel.user_id == user.user_id).first()
if not db_user:
logger.warning(f"Usuario no encontrado para actualizar: {user.user_id}")
return user
@@ -93,61 +131,75 @@ class UserRepositorySQL(UserRepository):
db_user.numero_reportes = user.numero_reportes
db_user.url_foto_perfil = user.url_foto_perfil
db_user.biografia = user.biografia
self.db.commit()
self.db.refresh(db_user)
db.commit()
db.refresh(db_user)
logger.info(f"Usuario actualizado exitosamente: {user.user_id}")
return self._to_domain(db_user)
except Exception as e:
self.db.rollback()
db.rollback()
logger.error(f"Error al actualizar usuario {user.user_id}: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def delete(self, user_id: int) -> bool:
"""Elimina un usuario con manejo de transacciones"""
db, is_own = self._get_session()
try:
db_user = self.db.query(UserModel).filter(UserModel.user_id == user_id).first()
db_user = db.query(UserModel).filter(UserModel.user_id == user_id).first()
if db_user:
self.db.delete(db_user)
self.db.commit()
db.delete(db_user)
db.commit()
logger.info(f"Usuario eliminado exitosamente: {user_id}")
return True
logger.warning(f"Usuario no encontrado para eliminar: {user_id}")
return False
except Exception as e:
self.db.rollback()
db.rollback()
logger.error(f"Error al eliminar usuario {user_id}: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def increment_reports(self, user_id: int) -> None:
"""Incrementa el contador de reportes con manejo de transacciones"""
db, is_own = self._get_session()
try:
db_user = self.db.query(UserModel).filter(UserModel.user_id == user_id).first()
db_user = db.query(UserModel).filter(UserModel.user_id == user_id).first()
if db_user:
db_user.numero_reportes += 1
self.db.commit()
db.commit()
logger.info(f"Contador de reportes incrementado para usuario: {user_id}")
else:
logger.warning(f"Usuario no encontrado para incrementar reportes: {user_id}")
except Exception as e:
self.db.rollback()
db.rollback()
logger.error(f"Error al incrementar reportes del usuario {user_id}: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def update_rating(self, user_id: int, new_rating: float) -> None:
"""Actualiza la calificación de un usuario con manejo de transacciones"""
db, is_own = self._get_session()
try:
db_user = self.db.query(UserModel).filter(UserModel.user_id == user_id).first()
db_user = db.query(UserModel).filter(UserModel.user_id == user_id).first()
if db_user:
# Asegurar que la calificación esté en el rango 0-100
db_user.calificacion = max(0, min(100, new_rating))
self.db.commit()
db.commit()
logger.info(f"Calificación actualizada para usuario {user_id}: {db_user.calificacion}")
else:
logger.warning(f"Usuario no encontrado para actualizar calificación: {user_id}")
except Exception as e:
self.db.rollback()
db.rollback()
logger.error(f"Error al actualizar calificación del usuario {user_id}: {e}", exc_info=True)
raise
finally:
if is_own:
db.close()
def _to_domain(self, db_user: UserModel) -> User:
"""Convierte un modelo SQLAlchemy a un objeto de dominio"""

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, status, Depends
from fastapi import APIRouter, HTTPException, status
from infrastructure.api.users.schemas import (
UserCreateRequest, UserUpdateRequest, UserResponse,
UserLoginRequest, UserLoginResponse
@@ -14,7 +14,6 @@ import logging
from datetime import datetime
router = APIRouter()
user_repo = UserRepositorySQL()
logger = logging.getLogger(__name__)
@@ -44,6 +43,7 @@ async def login_user(credentials: UserLoginRequest):
- email: Email confirmado
"""
try:
user_repo = UserRepositorySQL() # sesión nueva por request
get_use_case = GetUserByEmail(user_repo)
user = get_use_case.execute(credentials.email)
if not user:
@@ -74,6 +74,7 @@ async def create_user(user_data: UserCreateRequest):
- biografia: Biografía del usuario (opcional)
"""
try:
user_repo = UserRepositorySQL() # sesión nueva por request
create_use_case = CreateUser(user_repo)
result = create_use_case.execute(
nombre=user_data.nombre, apellido=user_data.apellido, email=user_data.email,
@@ -97,6 +98,7 @@ async def create_user(user_data: UserCreateRequest):
async def get_user(user_id: int):
"""Obtiene un usuario por ID"""
try:
user_repo = UserRepositorySQL() # sesión nueva por request
get_use_case = GetUserById(user_repo)
user = get_use_case.execute(user_id)
if not user:
@@ -112,6 +114,7 @@ async def get_user(user_id: int):
async def get_user_by_email(email: str):
"""Obtiene un usuario por email"""
try:
user_repo = UserRepositorySQL() # sesión nueva por request
get_use_case = GetUserByEmail(user_repo)
user = get_use_case.execute(email)
if not user:
@@ -127,6 +130,7 @@ async def get_user_by_email(email: str):
async def list_users():
"""Obtiene todos los usuarios - retorna lista vacía si no hay registros"""
try:
user_repo = UserRepositorySQL() # sesión nueva por request
list_use_case = ListAllUsers(user_repo)
return list_use_case.execute()
except Exception as e:
@@ -137,6 +141,7 @@ async def list_users():
async def update_user(user_id: int, user_data: UserUpdateRequest):
"""Actualiza un usuario - envía a cola de procesamiento con validaciones previas"""
try:
user_repo = UserRepositorySQL() # sesión nueva por request
update_use_case = UpdateUser(user_repo)
result = update_use_case.execute(user_id=user_id, nombre=user_data.nombre, apellido=user_data.apellido, url_foto_perfil=user_data.url_foto_perfil, biografia=user_data.biografia)
if result["status"] == "error":
@@ -156,6 +161,7 @@ async def update_user(user_id: int, user_data: UserUpdateRequest):
async def delete_user(user_id: int):
"""Elimina un usuario - envía a cola de procesamiento con validaciones previas"""
try:
user_repo = UserRepositorySQL() # sesión nueva por request
delete_use_case = DeleteUser(user_repo)
result = delete_use_case.execute(user_id)
if result["status"] == "error":