I'm trying to decide how to hide data layer details (joinedload
of sqlalchemy) in clean architecture.
I have some dilemma about clean architecture. Please look at the code of some api endpoint (full code in https://github.com/albertalexandrov/clean-architecture-sqlalchemy-details, framework - FastAPI):
# api/get_user.py
from fastapi import Depends
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app import app
from models.users import User
from repositories.users import UsersRepository
class Schema(BaseModel):
class ProfileSchema(BaseModel):
id: int
age: int
hobby: str
address: str
id: int
first_name: str
last_name: str
username: str
class Repository:
"""This endpoint repository that incapsulates getting user with options.
I had to create it because I had to use selectinload that is a detail of data layer (of SQLAlchemy)
and according to clean architecture service should not know about it
"""
def __init__(self, session: AsyncSession = Depends()):
self._session = session
self._users_repository = UsersRepository(session)
async def get_user(self, user_id: int):
options = [selectinload(User.profile)]
return await self._users_repository.get_by_pk(user_id, options)
class Service:
"""This endpoint service for demo purposes. It can be much more complex"""
def __init__(self, repository: Repository = Depends()):
self._repository = repository
async def get_user(self, user_id: int):
# some logic
user = await self._repository.get_user(user_id)
# some logic
return user
# another methods
@app.get("/user/{user_id}", response_model=Schema)
async def get_user(user_id: int, service: Service = Depends()):
return await service.get_user(user_id)
options
is SQLAlchemy instruction how to select related data (via sql join or make select in).
I split code into framework layer
(this is view, @app.get(...)
), service layer
(business logic) and data layer
(SQLAlchemy, repositories).
The users_repository:
class BaseRepository:
model = None # SQLAlchemy model
def __init__(self, session: AsyncSession = Depends()):
self._session = session
async def get_by_pk(self, pk, options=()):
return await self._session.get(self.model, pk, options=options)
# another methods
class UsersRepository(BaseRepository):
model = User
Please take a look at Repository.__init__
. As it is written there I did like there because I needed to hide details of SQLAlchemy from services layer (need to specify options
param to select related data).
If I do like this:
class Service:
"""This endpoint service for demo purposes. It can be much more complex"""
def __init__(self, users_repository: UsersRepository = Depends()):
self._users_repository = users_repository
async def get_user(self, user_id: int):
# some logic
options = [selectinload(User.profile)]
user = await self._users_repository.get_by_pk(user_id, options)
# some logic
return user
# another methods
then service layer knows about details of SQLAlchemy (how data layer works exactly).
The dilemma is 1) to create Repository
to specify options
inside Repository.get_user
method (thus service layer does not know details about SQLAlchemy) or 2) to specify options
inside Service.get_user
(then service layer knows about details of SQLAlchemy).
For know I create Repository
if I need to specify options
. Is that right?
So how to handle such situations?