Коротко — решающий фактор не «атрибут vs аргумент», а срок жизни сессии и срок жизни сервиса. Нельзя хранить сессию в долгоживущем/глобальном объекте. Если сервис создаётся заново для каждого запроса (через Depends), то сохранять AsyncSession как атрибут конструктора вполне удобно и безопасно. Если сервис живёт дольше (singleton, глобальный), то сессия должна передаваться в каждый метод.
Далее — подробнее с плюсами/минусами и практическими рекомендациями.
1) Сессия как атрибут сервиса (инициализировать в __init__)
- Плюсы:
- Меньше бойлерплейта при вызове методов — не надо передавать session везде.
- Удобно, если сервис создаётся per-request (через Depends) и живёт только одну обработку.
- Минусы:
- Категорически нельзя делать так, если сервис — singleton/глобальный: получится конкурентный доступ к одной сессии, race condition и утечки.
- Немного сложнее рефакторить/тестировать, если вы не используете DI (надо создавать сервис с mock-сессией).
- Когда подходит:
- Когда вы используете FastAPI Depends и создаёте сервис в DI каждую обработку: def get_service(session: AsyncSession = Depends(get_session)) -> Service: return Service(session)
2) Передавать сессию в каждый метод
- Плюсы:
- Явная зависимость — проще тестировать и очевидно, что методы не хранят состояние.
- Безопасно для любого срока жизни сервиса (включая singleton).
- Хорошо сочетается с функциональным стилем и unit-of-work.
- Минусы:
- Больше аргументов в сигнатурах методов — немного больше кода.
- Когда подходит:
- Если сервисы/репозитории могут быть долгоживущими, используются в фоновых задачах, или вы стремитесь к максимально явному дизайну.
3) Рекомендованный компромисс / практический паттерн
- Если вы проектируете типичное веб-приложение с per-request-сессией и зависимостями через FastAPI — делайте фабрику сервиса как dependency и передавайте сессию в конструктор. Так сохраняете удобство и безопасность:
- get_session — dependency с yield, открывает/закрывает сессию и/или делает begin/commit/rollback.
- get_service(session=Depends(get_session)) возвращает новый экземпляр Service(session).
- Если планируете использовать сервисы в разных контекстах (фоны, CLI, тесты), или хотите полностью явный контроль транзакций — передавайте session в методы.
4) Транзакции и scope сессии
- Обычно рекомендую разграничивать «создание/закрытие сессии» и «транзакции». Часто делают dependency, который:
- создаёт сессию,
- внутри yield вызывает session.begin() (или вручную commit/rollback в зависимости от исключений),
- закрывает сессию после ответа.
- Альтернатива — позволить сервисам/репозиториям открывать свои транзакции (unit of work).
5) Примеры (асинхронный SQLAlchemy + FastAPI)
Пример dependency для сессии:
```python
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_session() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
async with session.begin():
try:
yield session
except:
await session.rollback()
raise
```
Вариант A — сервис создаётся per-request, хранит session в атрибуте:
```python
class UserService:
def __init__(self, session: AsyncSession):
self.session = session
async def get_user(self, user_id: int):
return await self.session.get(User, user_id)
def get_user_service(session: AsyncSession = Depends(get_session)) -> UserService:
return UserService(session)
@router.get("/users/{user_id}")
async def read_user(user_id: int, svc: UserService = Depends(get_user_service)):
return await svc.get_user(user_id)
```
Вариант B — session передаётся в каждый метод:
```python
class UserService:
async def get_user(self, session: AsyncSession, user_id: int):
return await session.get(User, user_id)
@router.get("/users/{user_id}")
async def read_user(user_id: int, session: AsyncSession = Depends(get_session)):
svc = UserService()
return await svc.get_user(session, user_id)
```
6) Тестирование
- Вариант A: в тестах создаёте Service(session=fixture_session) — удобно.
- Вариант B: передаёте тестовую сессию в каждый вызов — более явный контроль.
7) Дополнительные замечания
- Не используйте глобальные или threadlocal сессии с async SQLAlchemy.
- Для фоновых задач создавайте новую сессию внутри задачи (не используйте request-session).
- Производительность: накладные расходы передачи параметра ничтожно малы; выбирайте архитектуру по читабельности и безопасности.
Вывод
- Если вы строите приложение по типичному FastAPI-дипендентному сценарию — безопасно и удобно инжектировать сессию в конструктор сервиса и создавать сервис per-request через Depends.
- Если сервисы могут жить дольше или вы хотите полностью явный и тестируемый дизайн — передавайте сессию в методы.
Если хотите, могу показать конкретный пример вашей архитектуры (репозитории + сервисы + зависимости) или помочь с шаблоном transaction/UnitOfWork для вашего проекта.