diff --git a/src/infrastructure/adapters/persistence/user_repository_sql.py b/src/infrastructure/adapters/persistence/user_repository_sql.py index 207d316..af1dd7c 100644 --- a/src/infrastructure/adapters/persistence/user_repository_sql.py +++ b/src/infrastructure/adapters/persistence/user_repository_sql.py @@ -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 @@ -9,12 +10,25 @@ 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,126 +43,164 @@ 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 - + db_user.nombre = user.nombre db_user.apellido = user.apellido db_user.calificacion = user.calificacion 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""" return User( @@ -163,4 +215,4 @@ class UserRepositorySQL(UserRepository): url_foto_perfil=db_user.url_foto_perfil, biografia=db_user.biografia, is_admin=db_user.is_admin - ) + ) \ No newline at end of file diff --git a/src/infrastructure/api/users/users.py b/src/infrastructure/api/users/users.py index 8521cf9..83cdaf6 100644 --- a/src/infrastructure/api/users/users.py +++ b/src/infrastructure/api/users/users.py @@ -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":