fixes with SQL Alchemy to avoid precaching
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
@@ -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":
|
||||||
|
|||||||
Reference in New Issue
Block a user