Escribí un artículo hace unos cinco meses sobre la adaptación del patrón de repositorio con FastAPI, y obtuve muchas lecturas (gracias). He venido a escribir sobre una forma eficiente de manejar el manejo de sesiones, aún usando el patrón de repositorio.
Antes de pasar directamente a esto, noté que en producción, obtengo cualquiera de los siguientes errores cada vez que mis API intentan realizar una transacción que implica leer o escribir en la base de datos:
Este error indica que hay una transacción no confirmada en curso que debe revertirse antes de continuar con cualquier otra operación de la base de datos.
La causa más común de este error es una excepción no controlada que se produce durante una transacción de la base de datos, lo que impide que la transacción se confirme o revierta correctamente.
Este error indica que una operación o solicitud que realizó a la base de datos no es válida o no se admite. Puede haber varias causas para este error, que incluyen:
Estoy seguro de que tiene ideas sobre cómo se pueden resolver estos errores, sin embargo, me gustaría decir que, aunque identifiqué cuál era el problema y lo solucioné, el problema persiste.
Si tiene curiosidad sobre cómo soluciono y resuelvo los problemas, puede considerar seguir los siguientes pasos:
Veamos cómo trabajé en una solución permanente que ha demostrado funcionar para mí. Procederé a usar un proyecto en el que trabajé cuando demostraba cómo usar patrones de repositorio.
Teníamos un módulo en el que almacenamos nuestra combinación de sesión base de orm con los siguientes códigos:
# SQLAlchemy Imports from sqlalchemy.orm import Session # Own Imports from config.database import SessionLocal from core.settings import ledger_settings class ORMSessionMixin: """Base orm session mixin for interacting with the database.""" def __init__(self): """ Get the next database session from the database pool. """ self.orm: Session = self.get_db().__next__() def get_db(self): """ This method creates a database session, yields it, rollback the transaction if there's an exception and then finally closes the session. Yields: db: scoped database session """ db = SessionLocal() try: yield db except Exception: db.rollback() finally: db.close()
El problema con esta solución era que si ocurre una excepción en el proceso de una transacción (esto podría ser cualquier cosa: crear un usuario, financiar su billetera, etc.), las excepciones no se manejan correctamente y la sesión de la base de datos en tránsito no obtener reversión.
Después de tres meses de depuración y parches y mucha investigación, finalmente pude construir una forma eficiente de manejar sesiones.
# SQLAlchemy Imports import sqlalchemy from sqlalchemy.orm import Session # Own Imports from config.database.connection import SessionLocal class DatabaseSessionMixin: """Database session mixin.""" def __enter__(self) -> Session: self.db = SessionLocal() return self.db def __exit__(self, exc_type, exc_val, exc_tb): try: if exc_type is not None: self.db.rollback() except sqlalchemy.exc.SQLAlchemyError: pass finally: self.db.close() SessionLocal.remove() def use_database_session(): return DatabaseSessionMixin()
En este código:
DatabaseSession
es una clase de administrador de contexto que maneja la sesión y garantiza que se cierre correctamente y se revierta en caso de error.
__enter__
inicializa la sesión y la devuelve.
__exit__
comprueba las excepciones y revierte la sesión si se produce una excepción. Luego cierra la sesión y la elimina de la sesión del ámbito.
use_database_session
es una función de utilidad que se puede usar como decorador o administrador de contexto para simplificar el uso de la sesión.
Aquí hay un ejemplo de cómo puede usar la función de utilidad use_database_session:
with use_database_session() as db: # perform logic that uses the session # ... # After exiting the context, the session will be automatically closed and removed from the scoped session.
El enfoque anterior proporciona una forma más limpia y eficiente de manejar las sesiones y garantiza que se reviertan o cierren correctamente en caso de error. Procedamos a cómo implementa el patrón de repositorio mientras usa la sesión de la base de datos en el ORM.
# SQLAlchemy Imports import sqlalchemy from sqlalchemy.orm import Session class BaseRepository: def __init__(self, session: Session): self.db = session class UserRepository(BaseRepository): """Operations to interact with the `users` table in the database.""" def get(self, user_id: int) -> User: """This method gets a user from the database.""" user = ( self.db.query(User) .filter(User.id == user_id) .first() ) return user def create(self, name: str, email: str, password: str) -> User: """This method creates a user.""" user = User(name=name, email=email, password=password) self.db.add(user) self.db.commit() self.db.refresh(user) return user def update_user(self, user_id: int, updated_data: dict): """This method updates a user.""" user = self.get(user_id) if user: for key, value in updated_data.items(): setattr(user, key, value) self.db.commit() return user return None def delete_user(self, user_id): """This method deletes a user.""" user = self.get_user(user_id) if user: self.db.delete(user) self.db.commit() return True return False
Lo siguiente sería integrar el repositorio anterior en la capa de servicio de su aplicación. Suponga que tiene una función de servicio que crea cuentas de usuarios; así es como lo haría usando nuestro nuevo método:
# Apps Imports from apps.users.models import User from apps.users.repo import UserRepository from apps.users.schemas.auth import UserCreate # Config Imports from config.security.hashers import password from config.database.session_mixin import use_database_session async def create_user(user: UserCreate) -> User: """ This function creates a new user in the database. :param user: schemas.UserCreate :type user: schemas.UserCreate :return: The user object """ with use_database_session() as db: users_repo = UserRepository(db) user = users_repo.create( user.name, user.email, password.hash(user.password) ) return user
El patrón anterior le permitirá encapsular las operaciones de la base de datos dentro de las clases del repositorio mientras aprovecha la sesión de la base de datos heredada. También proporciona una separación limpia entre sus modelos ORM y la lógica del repositorio.
En conclusión, el manejo eficiente de las sesiones es importante cuando se construyen sistemas back-end.
Los errores como sqlalchemy.exc.PendingRollbackError
y sqlalchemy.exc.InvalidRequestError
que ocurren durante las transacciones de la base de datos pueden generar inconsistencias en los datos y fallas en la aplicación si no se manejan correctamente.
Identificar y resolver estos errores es importante para mantener la integridad y confiabilidad del sistema.
Para abordar los problemas relacionados con el manejo de sesiones, es esencial implementar estrategias sólidas. Un enfoque es usar administradores de contexto, como DatabaseSessionMixin
que demostramos en el artículo.
Este administrador de contexto garantiza que las sesiones se abran, cierren y reviertan correctamente en caso de excepciones. Al encapsular la lógica de la sesión dentro del administrador de contexto, puede optimizar la administración de la sesión y mejorar el manejo de errores.
Además, la integración del patrón de repositorio en la capa de servicio de la aplicación puede mejorar aún más la eficiencia del manejo de sesiones.
Al separar las operaciones de la base de datos en clases de repositorio y aprovechar la sesión heredada del administrador de contexto, puede lograr una organización de código más limpia y mantener una separación clara entre los modelos ORM y la lógica del repositorio.
En general, el manejo eficiente de sesiones es crucial para mantener la consistencia de los datos, prevenir errores y garantizar la estabilidad de los sistemas de back-end.
Al seguir las mejores prácticas, como el uso de administradores de contexto y la adopción del patrón de repositorio, los desarrolladores pueden crear sistemas robustos y confiables que administren sesiones y manejen errores de manera efectiva durante las transacciones de la base de datos.
Estoy abierto a escribir trabajos y buscar activamente roles de contrato que impliquen construir con Python (Django, FastAPI, etc.).