Implementuje "czystą architekturę" w pythonie i zastanawiam się jak pracować z repozytoriami w use case'ach przy założeniu, że mogą pojawić się przypadki wymagające kilku repozytoriów korzystające z różnych źródeł danych oraz wymagające transakcji, żeby wszystkie operacje na repozytoriach w use case były atomowe.
Do obsługi transakcji zaimplementowałem klase unit_of_work.py
, której interfejs wygląda tak:
class AsyncUnitOfWorkProtocol(Protocol):
async def __aenter__(self) -> Self: ...
async def __aexit__(self, exc_type, exc, tb) -> None: ...
async def commit(self) -> None: ...
async def rollback(self) -> None: ...
Rozważam głównie dwie opcje:
Opcja 1:
Controller otwiera context manager klasy unit_of_work.py
tym samym otwierając transkacje. Wewnątrz tworzę use case przekazując repozytorium. Tutaj też są dwie opcje:
- Repozytorium importowane oddzielnie
- Repozytorium przypisane do pola klasy
unit_of_work.py
, żeby Controller nie musiał wiedzieć jak utworzyć instancje repozytorium
Przykład:
@router.post(
"/user/",
status_code=status.HTTP_201_CREATED,
)
async def create_user(user_data: CreateUser):
uow = AsyncSqlalchemyFirebaseUnitOfWork(async_session_factory, SagaFactory())
async with uow:
use_case = CreateUser(uow.user_repository)
await use_case.execute(input_dto)
# ...
Opcja 2:
Przekazanie do use case obiektu unit_of_work.py
zamiast repozytorium. To ma kilka implikacji:
- Use case jest odpowiedzialny za obsługę transkacji.
- Nie jest wiadome z góry z jakich repozytoriów korzysta use case. Otrzymuje obiekt uow, który zawiera wszystkie repozytoria obsługiwane przez ten uow.
-
commit
/rollback
są wykonywane wewnątrz use case, a nie w controllerze.
Na początku stosowałem podejście drugie, ale co w sytuacji, gdy będę potrzebował jedno repo z sqlalchemy, a drugie repo z jakiegoś mikroserwisu. Bedą wtedy dwie klasy: SqlalchemyUnitOfWork
i np. SagaUnitOfWork
, wtedy będę musiał przekazać dwa obiekty uow
do use case, a to już wprowadza mały bałagan, nawet nie wiadomo jak określić argumenty - uow_a
, uow_b
? Którego użyć żeby skorzystać z repo A? itp.
Często też błąd transakcji rzucany jest w momencie wywołania commit()
zatem to czy transakcja obsługiwana jest w use case czy w warstwie wyższej też ma znaczenie, np. w przypadku utworzenia transakcji w controllerze mógłbym dodać użytkownika do sesji w use case i wysłać email aktywacyjny, a po przekazaniu sterowania do controllera nastąpi wywołanie commit()
, które wywoła rollback()
jeśli wystąpi błąd i email zostanie wysłany, mimo, że użytkownik nie został utworzony.
Czy ktoś już rozwiązywał podobny problem i znalazł optymalne rozwiązanie?