0

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?

1 Answers1

2

I'm not sure where the dilemma is. Of course the repository should encapsulate the options and anything else database related. Returning only fully populated domain objects.

Ewan
  • 83,178