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 domain.users import User
from infrastructure.adapters.persistence.models import UserModel from infrastructure.adapters.persistence.models import UserModel
from infrastructure.adapters.persistence.db import SessionLocal from infrastructure.adapters.persistence.db import SessionLocal
from sqlalchemy.orm import Session
from typing import List, Optional from typing import List, Optional
import logging import logging
@@ -9,12 +10,25 @@ logger = logging.getLogger(__name__)
class UserRepositorySQL(UserRepository): class UserRepositorySQL(UserRepository):
"""Implementación del repositorio de Usuarios usando SQLAlchemy (MySQL)""" """Implementación del repositorio de Usuarios usando SQLAlchemy (MySQL)"""
def __init__(self, db_session=None): def __init__(self, db_session: Session = None):
self.db = db_session or SessionLocal() # 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: def save(self, user: User) -> User:
"""Guarda un nuevo usuario con manejo de transacciones""" """Guarda un nuevo usuario con manejo de transacciones"""
db, is_own = self._get_session()
try: try:
db_user = UserModel( db_user = UserModel(
nombre=user.nombre, nombre=user.nombre,
@@ -29,126 +43,164 @@ class UserRepositorySQL(UserRepository):
biografia=user.biografia, biografia=user.biografia,
is_admin=user.is_admin is_admin=user.is_admin
) )
self.db.add(db_user) db.add(db_user)
self.db.commit() db.commit()
self.db.refresh(db_user) db.refresh(db_user)
logger.info(f"Usuario guardado exitosamente: {db_user.user_id}") logger.info(f"Usuario guardado exitosamente: {db_user.user_id}")
return self._to_domain(db_user) return self._to_domain(db_user)
except Exception as e: except Exception as e:
self.db.rollback() db.rollback()
logger.error(f"Error al guardar usuario: {e}", exc_info=True) logger.error(f"Error al guardar usuario: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def find_by_id(self, user_id: int) -> Optional[User]: def find_by_id(self, user_id: int) -> Optional[User]:
"""Obtiene un usuario por ID""" """Obtiene un usuario por ID"""
db, is_own = self._get_session()
try: 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: if db_user:
return self._to_domain(db_user) return self._to_domain(db_user)
return None return None
except Exception as e: except Exception as e:
logger.error(f"Error al buscar usuario por ID {user_id}: {e}", exc_info=True) logger.error(f"Error al buscar usuario por ID {user_id}: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def find_by_email(self, email: str) -> Optional[User]: def find_by_email(self, email: str) -> Optional[User]:
"""Obtiene un usuario por email""" """Obtiene un usuario por email"""
db, is_own = self._get_session()
try: 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: if db_user:
return self._to_domain(db_user) return self._to_domain(db_user)
return None return None
except Exception as e: except Exception as e:
logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True) logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def find_by_email_with_password(self, email: str) -> Optional[UserModel]: 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)""" """Obtiene un usuario por email incluyendo el hash de contraseña (para autenticación)"""
db, is_own = self._get_session()
try: 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 return db_user
except Exception as e: except Exception as e:
logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True) logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def find_all(self) -> List[User]: def find_all(self) -> List[User]:
"""Obtiene todos los usuarios""" """Obtiene todos los usuarios"""
db, is_own = self._get_session()
try: try:
db_users = self.db.query(UserModel).all() db_users = db.query(UserModel).all()
return [self._to_domain(user) for user in db_users] return [self._to_domain(user) for user in db_users]
except Exception as e: except Exception as e:
logger.error(f"Error al obtener todos los usuarios: {e}", exc_info=True) logger.error(f"Error al obtener todos los usuarios: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def update(self, user: User) -> User: def update(self, user: User) -> User:
"""Actualiza un usuario con manejo de transacciones""" """Actualiza un usuario con manejo de transacciones"""
db, is_own = self._get_session()
try: 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: if not db_user:
logger.warning(f"Usuario no encontrado para actualizar: {user.user_id}") logger.warning(f"Usuario no encontrado para actualizar: {user.user_id}")
return user return user
db_user.nombre = user.nombre db_user.nombre = user.nombre
db_user.apellido = user.apellido db_user.apellido = user.apellido
db_user.calificacion = user.calificacion db_user.calificacion = user.calificacion
db_user.numero_reportes = user.numero_reportes db_user.numero_reportes = user.numero_reportes
db_user.url_foto_perfil = user.url_foto_perfil db_user.url_foto_perfil = user.url_foto_perfil
db_user.biografia = user.biografia db_user.biografia = user.biografia
self.db.commit() db.commit()
self.db.refresh(db_user) db.refresh(db_user)
logger.info(f"Usuario actualizado exitosamente: {user.user_id}") logger.info(f"Usuario actualizado exitosamente: {user.user_id}")
return self._to_domain(db_user) return self._to_domain(db_user)
except Exception as e: except Exception as e:
self.db.rollback() db.rollback()
logger.error(f"Error al actualizar usuario {user.user_id}: {e}", exc_info=True) logger.error(f"Error al actualizar usuario {user.user_id}: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def delete(self, user_id: int) -> bool: def delete(self, user_id: int) -> bool:
"""Elimina un usuario con manejo de transacciones""" """Elimina un usuario con manejo de transacciones"""
db, is_own = self._get_session()
try: 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: if db_user:
self.db.delete(db_user) db.delete(db_user)
self.db.commit() db.commit()
logger.info(f"Usuario eliminado exitosamente: {user_id}") logger.info(f"Usuario eliminado exitosamente: {user_id}")
return True return True
logger.warning(f"Usuario no encontrado para eliminar: {user_id}") logger.warning(f"Usuario no encontrado para eliminar: {user_id}")
return False return False
except Exception as e: except Exception as e:
self.db.rollback() db.rollback()
logger.error(f"Error al eliminar usuario {user_id}: {e}", exc_info=True) logger.error(f"Error al eliminar usuario {user_id}: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def increment_reports(self, user_id: int) -> None: def increment_reports(self, user_id: int) -> None:
"""Incrementa el contador de reportes con manejo de transacciones""" """Incrementa el contador de reportes con manejo de transacciones"""
db, is_own = self._get_session()
try: 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: if db_user:
db_user.numero_reportes += 1 db_user.numero_reportes += 1
self.db.commit() db.commit()
logger.info(f"Contador de reportes incrementado para usuario: {user_id}") logger.info(f"Contador de reportes incrementado para usuario: {user_id}")
else: else:
logger.warning(f"Usuario no encontrado para incrementar reportes: {user_id}") logger.warning(f"Usuario no encontrado para incrementar reportes: {user_id}")
except Exception as e: except Exception as e:
self.db.rollback() db.rollback()
logger.error(f"Error al incrementar reportes del usuario {user_id}: {e}", exc_info=True) logger.error(f"Error al incrementar reportes del usuario {user_id}: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def update_rating(self, user_id: int, new_rating: float) -> None: def update_rating(self, user_id: int, new_rating: float) -> None:
"""Actualiza la calificación de un usuario con manejo de transacciones""" """Actualiza la calificación de un usuario con manejo de transacciones"""
db, is_own = self._get_session()
try: 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: if db_user:
# Asegurar que la calificación esté en el rango 0-100
db_user.calificacion = max(0, min(100, new_rating)) 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}") logger.info(f"Calificación actualizada para usuario {user_id}: {db_user.calificacion}")
else: else:
logger.warning(f"Usuario no encontrado para actualizar calificación: {user_id}") logger.warning(f"Usuario no encontrado para actualizar calificación: {user_id}")
except Exception as e: 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) logger.error(f"Error al actualizar calificación del usuario {user_id}: {e}", exc_info=True)
raise raise
finally:
if is_own:
db.close()
def _to_domain(self, db_user: UserModel) -> User: def _to_domain(self, db_user: UserModel) -> User:
"""Convierte un modelo SQLAlchemy a un objeto de dominio""" """Convierte un modelo SQLAlchemy a un objeto de dominio"""
return User( return User(
@@ -163,4 +215,4 @@ class UserRepositorySQL(UserRepository):
url_foto_perfil=db_user.url_foto_perfil, url_foto_perfil=db_user.url_foto_perfil,
biografia=db_user.biografia, biografia=db_user.biografia,
is_admin=db_user.is_admin is_admin=db_user.is_admin
) )

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